currencycloud / currencycloud-java Goto Github PK
View Code? Open in Web Editor NEWA Java wrapper around the v2 API
License: MIT License
A Java wrapper around the v2 API
License: MIT License
When updating a resource (POST /{id}), the Ruby library seems to (I don't speak Ruby) send only the properties that were actually updated (or skip the updated altogether if there were none). Should the Java library do this too?
Hey CC team, is it reasonable to introduce toggle for PII fields in exception logs (ex. iban, bic, switft etc.) ?
The problem is related to create/update/validate beneficiary;
When I provide the beneficiary’s date of birth value I always have the following error:
“Invalid extra parameters:‘{:“beneficiary_date_of_birth\tdate”=>“1986-12-12"}’”
I think there is a bug in the sdk because in the CurrencyCloud.class the field beneficiaryDateOfBirth is binded with the value “beneficiary_date_of_birth date” where I think should be only “beneficiary_date_of_birth”.
The methods are : ” createBeneficiary(…), validateBeneficiary(…), updateBeneficiary(…)
@org.junit.Test public void authroizePayment() { List<String> paymentIdList = new ArrayList<>(); paymentIdList.add("fec2f9c4-7f92-4d11-ac96-39e441aafbc9"); PaymentAuthorisations paymentAuthorisations = currencyCloud.authorisePayment(paymentIdList); for (PaymentAuthorisation paymentAuthorisation : paymentAuthorisations.getPaymentAuthorisations()) { System.err.println(paymentAuthorisation.toString()); } }
platform: "Java 1.8.0_202 (Oracle Corporation)"
request:
parameters:
paymentIds[]: "fec2f9c4-7f92-4d11-ac96-39e441aafbc9"
verb: "post"
url: "https://devapi.currencycloud.com/v2/payments/authorise?paymentIds[]=fec2f9c4-7f92-4d11-ac96-39e441aafbc9"
response:
status_code: 400
date: "Tue, 16 Jul 2019 06:51:10 GMT"
request_id: "c03afe4d-7884-4cfc-9b3c-1e6aa51cb665"
error_code: "invalid_extra_parameters"
errors:
How can I deal with this? Is there the server or SDK error? I use sdk 3.2.2
Run existing tests with coverage, see what's not covered, cover it.
The requirements for #21 as per http://central.sonatype.org/pages/requirements.html include:
Jackson provides this check.
There are many parameters/properties in the API that are declared as Strings, but can take a limited set of values (eg. country codes, currencies, custom statuses etc.). See if it makes sense to implement them as enums. Going with Strings for the time being.
These are handled automatically in the Ruby client. Implement the same here?
It seems many things could be validated on the client. Bean validation could be used for this.
Please read the detailed instructions. This is a short summary of what needs to be done:
gpg --gen-key
gpg --list-keys
gpg2 --edit-key C6EED57A # delete subkeys if necessary -- please see the detailed instructions
> key 1
> delkey
gpg --keyserver hkp://pool.sks-keyservers.net --send-keys C6EED57A # replace with your keyid from --list-keys
This is only needed for final releases (we can publish snapshots unsigned).
The GPG key will be used to sign artifacts on a local machine (where the key is stored); I'll provide instructions to do this later. Eg., the GPG passphrase will need to be saved locally in Maven's ~/.m2/settings.xml.
I am facing issue in
currencyCloudClient.createAccount(account)
It is throwing exception (exception attached below) and despite on the demo account it creates a sub account.
I am using this file src/test/java/com/currencycloud/examples/CurrencyCloudCookbook.java
And added these few lines of code.
Account account = client.createAccount(new Account(
"Coockbook",
"company",
"BLR",
"BLR",
"560068",
"IN"
)
);
Exception in thread "main" ---
exception_type: "UnexpectedException"
platform: "Java 18.0.1.1 (Oracle Corporation)"
request: null
inner_error: "java.lang.reflect.InaccessibleObjectException: Unable to make protected\
\ final java.lang.Class java.lang.ClassLoader.defineClass(java.lang.String,byte[],int,int,java.security.ProtectionDomain)\
\ throws java.lang.ClassFormatError accessible: module java.base does not \"opens\
\ java.lang\" to unnamed module @307f6b8c"
at com.currencycloud.client.ExceptionTransformer.aroundInvoke(ExceptionTransformer.java:23)
at si.mazi.rescu.InterceptedInvocationHandler.invoke(InterceptedInvocationHandler.java:42)
at com.currencycloud.client.Reauthenticator.aroundInvoke(Reauthenticator.java:23)
at si.mazi.rescu.InterceptedInvocationHandler.invoke(InterceptedInvocationHandler.java:42)
at jdk.proxy2/jdk.proxy2.$Proxy4.createAccount(Unknown Source)
at com.currencycloud.client.CurrencyCloudClient.createAccount(CurrencyCloudClient.java:157)
at com.currencycloud.examples.CurrencyCloudCookbook.runCookBook(CurrencyCloudCookbook.java:51)
at com.currencycloud.examples.CurrencyCloudCookbook.main(CurrencyCloudCookbook.java:30)
Model classes have not been designed to support object equality, and we want to find out if that would be useful to sdk users.
An immediate benefit would be proper and efficient manipulation in collections. Are there other use cases? Should all or some classes support it? If some, which?
The starting point could be the README.md. If it's big, it could be split into wiki pages on GitHub.
Also provide:
The Ruby library has a notion of UnexpectedError. Should we mimic this in the Java library?
Try to use the existing Ruby specifications.
When calling CurrencyCloudClient.getPaymentTrackingInfo
, we get the following error generated:
exception_type: "ApiException"
platform: "Java 17.0.4 (Eclipse Adoptium)"
request:
parameters: {}
verb: "get"
url: "https://api.currencycloud.com/v2/payments/<payment-id>/tracking_info"
response:
status_code: 200
date: null
request_id: null
error_code: null
errors: []
at com.currencycloud.client.exception.ApiException.create(ApiException.java:71)
at com.currencycloud.client.ExceptionTransformer.aroundInvoke(ExceptionTransformer.java:19)
at si.mazi.rescu.InterceptedInvocationHandler.invoke(InterceptedInvocationHandler.java:42)
at com.currencycloud.client.Reauthenticator.aroundInvoke(Reauthenticator.java:23)
at si.mazi.rescu.InterceptedInvocationHandler.invoke(InterceptedInvocationHandler.java:42)
at jdk.proxy2/jdk.proxy2.$Proxy24.getPaymentTrackingInfo(Unknown Source)
at com.currencycloud.client.CurrencyCloudClient.getPaymentTrackingInfo(CurrencyCloudClient.java:1057)
at com.bndrts.service.external.CurrencycloudWrapper.lambda$getPaymentTrackingInfo$7(CurrencycloudWrapper.java:124)
at com.bndrts.service.external.CurrencycloudWrapper.lambda$callOnBehalfOf$11(CurrencycloudWrapper.java:154)
at com.currencycloud.client.CurrencyCloudClient.onBehalfOfDo(CurrencyCloudClient.java:121)
at com.bndrts.service.external.CurrencycloudWrapper.callOnBehalfOf(CurrencycloudWrapper.java:151)
at com.bndrts.service.external.CurrencycloudWrapper.getPaymentTrackingInfo(CurrencycloudWrapper.java:123)
at com.bndrts.service.external.CCPaymentInstructionService.paymentReceived(CCPaymentInstructionService.java:226)
at com.bndrts.service.external.CCPaymentInstructionService.processPayment(CCPaymentInstructionService.java:211)
at java.base/java.util.ArrayList.forEach(Unknown Source)
at com.bndrts.service.external.CCPaymentInstructionService.processCompletedSwiftPayments(CCPaymentInstructionService.java:203)
at com.bndrts.service.external.SwiftPaymentReceivedJob.execute(SwiftPaymentReceivedJob.java:24)
at com.bndrts.messaging.consumer.ScheduledEventHandler.lambda$execute$1(ScheduledEventHandler.java:43)
at com.bndrts.service.infrastructure.MDCPropagatingExecutorServiceDecorator.lambda$withMDC$0(ExecutorFactory.java:152)
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(Unknown Source)
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(Unknown Source)
at java.base/java.lang.Thread.run(Unknown Source)
Caused by: ResponseException{errorCode='null', errorMessages=null}
at java.base/jdk.internal.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
at java.base/jdk.internal.reflect.NativeConstructorAccessorImpl.newInstance(Unknown Source)
at java.base/jdk.internal.reflect.DelegatingConstructorAccessorImpl.newInstance(Unknown Source)
at java.base/java.lang.reflect.Constructor.newInstanceWithCaller(Unknown Source)
at java.base/java.lang.reflect.Constructor.newInstance(Unknown Source)
at com.fasterxml.jackson.databind.introspect.AnnotatedConstructor.call(AnnotatedConstructor.java:123)
at com.fasterxml.jackson.databind.deser.std.StdValueInstantiator.createUsingDefault(StdValueInstantiator.java:278)
at com.fasterxml.jackson.databind.deser.std.ThrowableDeserializer.deserializeFromObject(ThrowableDeserializer.java:145)
at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:184)
at com.fasterxml.jackson.databind.deser.DefaultDeserializationContext.readRootValue(DefaultDeserializationContext.java:323)
at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:4674)
at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3629)
at si.mazi.rescu.serialization.jackson.JacksonResponseReader.read(JacksonResponseReader.java:53)
at si.mazi.rescu.serialization.jackson.JacksonResponseReader.readException(JacksonResponseReader.java:58)
at si.mazi.rescu.ResponseReader.read(ResponseReader.java:82)
at si.mazi.rescu.RestInvocationHandler.mapInvocationResult(RestInvocationHandler.java:175)
at si.mazi.rescu.RestInvocationHandler.receiveAndMap(RestInvocationHandler.java:163)
at si.mazi.rescu.RestInvocationHandler.invoke(RestInvocationHandler.java:119)
at com.currencycloud.client.AutoAuthenticator.aroundInvoke(AutoAuthenticator.java:26)
at si.mazi.rescu.InterceptedInvocationHandler.invoke(InterceptedInvocationHandler.java:42)
at com.currencycloud.client.ExceptionTransformer.aroundInvoke(ExceptionTransformer.java:17)
... 20 more
Digging a bit deeper, the root cause seems to be that we're getting an empty array for the charge_amount
field in our payment_events
:
{
"uetr": "...",
"transaction_status": {
"status": "completed",
"reason": null
},
"initiation_time": "2022-09-06T00:16:20+00:00",
"completion_time": "2022-09-06T07:52:57+00:00",
"last_update_time": "2022-09-06T07:53:17+00:00",
"payment_events": [
{
"tracker_event_type": "customer_credit_transfer_payment_status_update",
"valid": true,
"transaction_status": {
"status": "completed",
"reason": null
},
"funds_available": "2022-09-06T07:52:00+00:00",
"forwarded_to_agent": null,
"from": "...",
"to": "...",
"originator": "...",
"serial_parties": {
"debtor": null,
"debtor_agent": null,
"intermediary_agent1": null,
"instructing_reimbursement_agent": null,
"creditor_agent": null,
"creditor": null
},
"sender_acknowledgement_receipt": "2022-09-06T07:52:57+00:00",
"instructed_amount": null,
"confirmed_amount": {
"currency": "USD",
"amount": "..."
},
"interbank_settlement_amount": null,
"interbank_settlement_date": null,
"charge_amount": [],
"charge_type": null,
"foreign_exchange_details": null,
"last_update_time": "2022-09-06T07:53:17+00:00"
},
...
The SDK is attempting to deserialise this empty array to a com.currencycloud.client.model.PaymentTrackingInfo.Amount
object which then fails with a Jackson exception:
Exception in thread "main" com.fasterxml.jackson.databind.exc.MismatchedInputException: Cannot deserialize value of type `com.currencycloud.client.model.PaymentTrackingInfo$Amount` from Array value (token `JsonToken.START_ARRAY`)
Currently collections are serialized like this:
payment_types=regular,another
but should be like this:
payment_types[]=regular&payment_types[]=another
This should at least cover something like the cookbook, but preferably all API calls not yet covered by the Betamax tests.
GET on payment_dates is missing.
Refer to here:
https://developer.currencycloud.com/documentation/api-docs/reference/get-reference-payment_dates/
/** Create a Payment */
@post
@path("payments/create")
@consumes(MediaType.APPLICATION_FORM_URLENCODED)
Payment createPayment(
@HeaderParam("X-Auth-Token") String authToken,
@FormParam("currency") String currency,
@FormParam("beneficiary_id") String beneficiaryId,
@FormParam("amount") BigDecimal amount,
@FormParam("reason") String reason,
@FormParam("reference") String reference,
@nullable @FormParam("payment_date") java.sql.Date paymentDate,
@nullable @FormParam("payment_type") String paymentType,
@nullable @FormParam("conversion_id") String conversionId,
@nullable @FormParam("payer_entity_type") String payerEntityType,
@nullable @FormParam("payer_company_name") String payerCompanyName,
@nullable @FormParam("payer_first_name") String payerFirstName,
@nullable @FormParam("payer_last_name") String payerLastName,
@nullable @FormParam("payer_city") String payerCity,
@nullable @FormParam("payer_postcode") String payerPostcode,
@nullable @FormParam("payer_state_or_province") String payerStateOrProvince,
@nullable @FormParam("payer_date_of_birth") java.sql.Date payerDateOfBirth,
@nullable @FormParam("payer_country") String payerCountry,
@nullable @FormParam("payer_address[]") List payerAddress,
@nullable @FormParam("payer_identification_type") String payerIdentificationType,
@nullable @FormParam("payer_identification_value") String payerIdentificationValue,
@nullable @FormParam("on_behalf_of") String onBehalfOf
) throws ResponseException;
public Payment createPayment(Payment payment, @nullable Payer payer) throws CurrencyCloudException {
if (payer == null) {
payer = Payer.create();
}
return api.createPayment(authToken,
payment.getCurrency(),
payment.getBeneficiaryId(),
payment.getAmount(),
payment.getReason(),
payment.getReference(),
dateOnly(payment.getPaymentDate()),
payment.getPaymentType(),
payment.getConversionId(),
payer.getLegalEntityType(),
payer.getCompanyName(),
payer.getFirstName(),
payer.getLastName(),
payer.getCity(),
payer.getPostcode(),
payer.getStateOrProvince(),
dateOnly(payer.getDateOfBirth()),
payer.getCountry(),
payer.getAddress(),
payer.getIdentificationType(),
payer.getIdentificationValue(),
onBehalfOf
);
}
Right now the only way to create CurrencyCloudClient is to pass CurrencyCloudClient.Environment as first argument to constructor. It would be handy to make it possible to pass custom url or make second constructor protected
@post
@path("beneficiaries/validate")
@consumes(MediaType.APPLICATION_FORM_URLENCODED)
Beneficiary validateBeneficiary(
@HeaderParam("X-Auth-Token") String authToken,
@FormParam("bank_country") String bankCountry,
@FormParam("currency") String currency,
@FormParam("beneficiary_country") String beneficiaryCountry,
@nullable @FormParam("account_number") String accountNumber,
@nullable @FormParam("routing_code_type_1") String routingCodeType1,
@nullable @FormParam("routing_code_value_1") String routingCodeValue1,
@nullable @FormParam("routing_code_type_2") String routingCodeType2,
@nullable @FormParam("routing_code_value_2") String routingCodeValue2,
@nullable @FormParam("bic_swift") String bicSwift,
@nullable @FormParam("iban") String iban,
@nullable @FormParam("bank_address[]") List bankAddress,
@nullable @FormParam("bank_name") String bankName,
@nullable @FormParam("bank_account_type") String bankAccountType,
@nullable @FormParam("beneficiary_entity_type") String beneficiaryEntityType,
@nullable @FormParam("beneficiary_company_name") String beneficiaryCompanyName,
@nullable @FormParam("beneficiary_first_name") String beneficiaryFirstName,
@nullable @FormParam("beneficiary_last_name") String beneficiaryLastName,
@nullable @FormParam("beneficiary_city") String beneficiaryCity,
@nullable @FormParam("beneficiary_postcode") String beneficiaryPostcode,
@nullable @FormParam("beneficiary_address[]") List beneficiaryAddress,
@nullable @FormParam("beneficiary_state_or_province") String beneficiaryStateOrProvince,
@nullable @FormParam("beneficiary_date_of_birth date") Date beneficiaryDateOfBirth,
@nullable @FormParam("beneficiary_identification_type") String beneficiaryIdentificationType,
@nullable @FormParam("beneficiary_identification_value") String beneficiaryIdentificationValue,
@nullable @FormParam("payment_types[]") List paymentTypes,
@nullable @FormParam("on_behalf_of") String onBehalfOf
) throws ResponseException;
public Beneficiary validateBeneficiary(Beneficiary beneficiary) throws CurrencyCloudException {
return api.validateBeneficiary(
authToken,
beneficiary.getBankCountry(),
beneficiary.getCurrency(),
beneficiary.getBeneficiaryCountry(),
beneficiary.getAccountNumber(),
beneficiary.getRoutingCodeType1(),
beneficiary.getRoutingCodeValue1(),
beneficiary.getRoutingCodeType2(),
beneficiary.getRoutingCodeValue2(),
beneficiary.getBicSwift(),
beneficiary.getIban(),
beneficiary.getBankAddress(),
beneficiary.getBankName(),
beneficiary.getBankAccountType(),
beneficiary.getBeneficiaryEntityType(),
beneficiary.getBeneficiaryCompanyName(),
beneficiary.getBeneficiaryFirstName(),
beneficiary.getBeneficiaryLastName(),
beneficiary.getBeneficiaryCity(),
beneficiary.getBeneficiaryPostcode(),
beneficiary.getBeneficiaryAddress(),
beneficiary.getBeneficiaryStateOrProvince(),
beneficiary.getBeneficiaryDateOfBirth(),
beneficiary.getBeneficiaryIdentificationType(),
beneficiary.getBeneficiaryIdentificationValue(),
beneficiary.getPaymentTypes(),
onBehalfOf
);
}
This requires #29 .
On every push, Travis should:
Most methods within the com.currencycloud.client.CurrencyCloudClient
pull the sub-account ID from the onBehalfOf
thread-local variable. This doesn't seem to be the case for the findFundingAccounts method, despite it being a supported field on the API call.
Any chance we could get it added?
Creating some example code / tutorial, like an implementation of the Cookbook, might be appropriate.
I already have the code, but need to decide on a good way to provide this. I see two options:
Solution 1. Is cleaner and more informative for beginners, with less clutter (like testing annotations), but is a bit harder to maintain (eg. SDK refactorings might break the example). 2. is simpler to maintain (automatically refactored when the SDK is refactored) and also provides some value to development as it can be used as a regression test.
This is what needs to be done to publish the project artifacts (#21). The detailed instructions can be found at http://central.sonatype.org/pages/ossrh-guide.html#initial-setup .
This is required for both snapshot and final releases.
If "artifact id" is requested, the value is currencycloud-java. If PGP (public) keys are requested, see #27.
The account username and password will need to be encrypted and provided to Travis -- I'll provide instructions for this later.
Hello CurrencyCloud team :),
You should consider removing logback.xml
from the package and instead letting users configure their own logging.
I encountered an issue when setting up a Spring Boot app which didn't appear to log anything, traced it to currencycloud-java. :)
Sonatype credentials (the same as for their Jira) are required to push the artifacts to the Maven repository.
Please test that this works.
You need Java 7+ SDK, git and Maven 3 installed.
Add the credentials to ~/.m2/settings.xml
:
<settings>
...
<servers>
<server>
<id>ossrh</id>
<username>your-jira-id</username>
<password>your-jira-pwd</password>
</server>
</servers>
...
</settings>
Then run this:
git clone [email protected]:CurrencyCloud/currencycloud-java.git
cd currencycloud-java
mvn clean deploy
For Travis to push snapshot artifacts to the Sonaype maven repo automatcially (#30), it needs to know the CurrencyCloud's Sonatype credentials. Travis provides a way to supply these credentials in an encrypted form publicly (via the GitHub repo) so that only Travis can access them. Instructions how to do this follow. The easiest (described below) uses the Travis Ruby gem.
Please note that if the Sonatype password (or username) contains symbols with special meaning in shell scripting such as braces, parentheses, backslashes, and pipe symbols, these must be escaped as described here.
gem install travis # use sudo if necessary
cd currencycloud-java # the project code directory
travis encrypt CI_DEPLOY_USERNAME=<CurrencyCloud Sonatype username>
travis encrypt CI_DEPLOY_PASSWORD=<CurrencyCloud Sonatype password>
The last two commands will output something like secure: ".... encrypted data ...."
. Please copy both of these values and paste them here.
Some of the method calls in the current Java API are a bit verbose and not easy to read; provide a better alternative.
Example of current API call:
Beneficiary beneficiary = client.updateBeneficiary(
"081596c9-02de-483e-9f2a-4cf55dcdf98c", "Test User 2",
null, null, null, null, null, null, null, null, null,
null, null, null, null, null, null, null,
null, null, "Acme Inc.", null, null, "Manchester",
null, null, null, null, null, null
);
A possible alternative:
Beneficiary beneficiary = client.updateBeneficiary(
new Beneficiary.Builder()
.Id("081596c9-02de-483e-9f2a-4cf55dcdf98c")
.BankAccountHolderName("Test User 2")
.BeneficiaryCompanyName("Acme Inc.")
.BeneficiaryCity("Manchester")
.build());
This can be done automatically using Travis.
Hey all,
Currently in prod we are getting: 400 Terms and conditions accepted is required when creating a new sub-account.
We seem to be missing the terms_and_conditions_accepted
needed in order to create an account per:
terms_and_conditions_accepted | formData | boolean | Acceptance of the terms and conditions. Required for sub-accounts that are on our Outsourced KYC model, optional otherwise. |
---|
https://developer.currencycloud.com/docs/item/create-account/
Probably need to add it here:
But will leave that to you all 😄
Thanks,
Hasnain
Lightyear
The Java SDK is not thread safe for on behalf calls. This means ownership of actions carried out on the API using on behalf functionality could be wrong in a multi-threaded setup.
The aim is to update the on behalf call so it is thread safe and still supports creating one client across multiple workers.
Here is some unit test code to show collisions when multiple threads try to access a single instance of "CurrencyCloudClient.onBehalfDo" (credit: @derikvercueil)
package com.currencycloud.client;
import org.junit.Assert;
import org.junit.Test;
/**
*
* Class to simulate multiple threads accessing a single CurrencyCloudClient instance.
*
*/
class TestThread extends Thread {
private final String contactId;
private final int threadId;
private final CurrencyCloudClient client;
public TestThread(int threadId, CurrencyCloudClient client, String contactId) {
this.client = client;
this.threadId = threadId;
this.contactId = contactId;
}
@Override
public void run() {
System.out.println("TestThread \"" + threadId + "\" running.");
int i=0;
while (i<1000) {
try {
client.onBehalfOfDo(contactId, new Runnable() {
@Override
public void run() {
String onBehalfOf = client.getOnBehalfOf();
/**
* It is always expected that \"threadId\" = 2nd last character of onBehalf to ensure that the Runnable for that Thread is running
*/
Assert.assertEquals("\"client.onBehalfOfDo\" Runnable executing for thread \"" + threadId + "\". \"onBehalfOf\"=\"" + onBehalfOf + "\"", threadId, Integer.parseInt(onBehalfOf.substring(onBehalfOf.length() - 2, onBehalfOf.length() - 1)));
}
});
i++;
} catch (Exception e) {
System.err.println(e.getMessage());
}
}
}
}
/**
* Test multiple threads attempting to access a single instance of the CurrencyCloudClient class.
*
*/
public class CurrencyCloudClientConcurrencyTest {
protected CurrencyCloudClient client = new CurrencyCloudClient("http://localhost:5555", null, null);
@Test
public void testConcurrentAccess() throws Exception {
TestThread[] threads = new TestThread[10];
int counter = 0;
for (TestThread thread : threads) {
thread = new TestThread(counter, client, "f57b2d33-652c-4589-a8ff-7762add270" + counter + "d");
thread.start();
counter++;
}
}
}
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.