Git Product home page Git Product logo

openapi-client's People

Contributors

jhthorsen avatar kiwiroy avatar manwar avatar mohawk2 avatar rabbiveesh avatar reneeb avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar

openapi-client's Issues

Mixing in roles to generated classes

The Problem

I've been setting up API clients for amazon's advertising APIs, and in the course of doing so, it was most convenient to mix in certain shared behaviors with a role.

The issue that I'm having is that I can't mix in roles nicely to the generated classes. For example:

OpenAPI::Client->with_roles('My::Awesome::Role')->new('path/to/spec.yaml')

Will still return a subclass of OpenAPI::Client, rather than a subclass of the package created by mixing in my role.

The alternative of

OpenAPI::Client->new('path/to/spec.yaml')->with_roles('My::Awesome::Role')

works, except that the behavior is surprising, b/c the role never gets instantiated, so the defaults for fields in the role are not set.

For reference, I use Moo::Role to create attributes (access to redis for storing access tokens, for example).

Ideas

Either the constructor for OpenAPI::Client should use a dynamic classname when generating the child class on lines 71-74, or there should be an extra argument for putting more stuff into the generated class.
Or perhaps give access to the generated class before instantiating it?

Thanks for this wonderful module!

POD spelling test fails (only centos & fedora)

t/00-project.t fails like this, only on RedHat-like systems (seen on fedora 36, centos 7 and rocky 8):

#   Failed test 'POD spelling for blib/lib/OpenAPI/Client.pm'
#   at t/00-project.t line 38.
# Errors:
#     Storey
#     v2
#     v3
# Looks like you failed 1 test of 12.
t/00-project.t .............. 
Dubious, test returned 1 (wstat 256, 0x100)

Duplicate keys not allowed

The test suite fails on some of my smokers:

Duplicate keys not allowed, at character offset 595 (before "required": ["email",...") at /home/cpansand/.cpan/build/2018070703/Mojolicious-7.87-ThKlUI/blib/lib/Mojo/JSON.pm line 39.
t/body-validation.t ......... 
Dubious, test returned 255 (wstat 65280, 0xff00)
No subtests run 

The possibly problematic files in the test file:

              "required": true,                                                                                                                                                                                                                                               
              "required": ["email", "password"],                                                                                                                                                                                                                              

OpenAPI 3 support

Looks like OpenAPI::Client doesn't currently support OpenAPI 3. Would be cool if it did, because Mojolicious::Plugin::OpenAPI does, and it's a one-way conversion... which means my Grand Plan(tm) of autogenerating the client-side API for the OpenAPI thing won't work :-/

Would this be very complicated?

Cannot set Mojo::URL host or scheme after construction when constructing client with base_url param

When constructing an OpenAPI::Client with a bare URL as the value of the base_url param, the host and scheme cannot be changed after construction.

However, if you instead construct a Mojo::URL object and pass that as a base_url, the methods can be accessed.

I'm including a test case that I wrote that demonstrates the issue below:

use Test::More;
use OpenAPI::Client;

my $client1;
my $client2;
my $base_url1 = Mojo::URL->new('http://example.com');
my $base_url2 = 'http://example.com';
my $otherhost = 'somewhereelse.com';

# Client 1: Use a Mojo::URL object for base_url

# Construct a client with a Mojo::URL object
ok($client1 = OpenAPI::Client->new("file://testswagger.yaml", base_url => $base_url1), "Constructed client1 - OpenAPI::Client with Mojo::URL object");
  
# Try to set client to use a different host
ok($client1->base_url->host($otherhost), "Set client1 to use host $host");

# Try to set client to use HTTPS
ok($client1->base_url->scheme('https'), "Set client1 to use https");

# Client 2: Use a bare URL for base_url

# Construct a client with a bare URL
ok($client2 = OpenAPI::Client->new("file://testswagger.yaml", base_url => $base_url2), "Constructed client2 - OpenAPI::Client with bare URL");
  
# Try to set client1 to use a different host
ok($client2->base_url->host($host), "Set client2 to use $host");

# Try to set client1 to use HTTPS
ok($client2->base_url->scheme('https'), "Set client2 to use https");

done_testing();

The test swagger spec:

swagger: '2.0'
info:
  version: 1.0.0
  title: Simple example API
  description: An API to illustrate Swagger
paths:
  /list:
    get:
      description: Returns a list of stuff              
      responses:
        200:
          description: Successful response

Mojo::UserAgent attribute in constructor ignored for the constructor

I'm trying to connect to a Kubernetes API server. This requires me to do two things:

  • Pass a token in a header
  • Set the API server's CA certificate

This is even true when trying to access the OpenAPI definition (at $apiserver/openapi/v2).

In order to do that, I prepared a Mojo::UserAgent like so:

my $token;
{
  local $/="";
  open my $token_f, "</run/secrets/kubernetes.io/serviceaccount/token";
  $token=<$token_f>
}
my $ua = Mojo::UserAgent->new;
$ua->on(prepare => sub($ua, $tx) {
  $tx->req->headers->header("Authorization: Bearer $token");
});
$ua = $ua->ca("/run/secrets/kubernetes.io/serviceaccount/ca.crt");
my $client = OpenAPI::Client->new("https://" . $ENV{KUBERNETES_SERVICE_HOST} . ":" . $ENV{KUBERNETES_SERVICE_PORT} . "/openapi/v2", ua => $ua);

However, OpenAPI::Client does not seem to use the Mojo::UserAgent at this point:

root@perl:~# perl ./test 
GET https://10.152.183.1:443/openapi/v2: SSL connect attempt failed error:1416F086:SSL routines:tls_process_server_certificate:certificate verify failed

One can bypass that by setting MOJO_CA_FILE, but that misses the header:

root@perl:~# MOJO_CA_FILE=/run/secrets/kubernetes.io/serviceaccount/ca.crt perl ./test 
GET https://10.152.183.1:443/openapi/v2: Forbidden at /usr/local/lib/perl5/site_perl/5.36.0/JSON/Validator/Store.pm line 190.

pre_processor enhancement.

HI,

Nowadays, there are many services that want the header encrypted with api key like https://metacpan.org/pod/WebService::Cryptopia

    my $signature = $self->api_key . "POST" . lc( url_encode( $url ) ) . $nonce . $request_content_base64_string;
    chomp( $signature );
    $self->log->trace( "Signature: $signature" ) if $self->log->is_trace;
    my $hmac_signature = encode_base64( hmac_sha256( $signature, decode_base64( $self->api_secret ) ) );
    chomp( $hmac_signature );
    $self->log->trace( "HMAC signature: $hmac_signature" ) if $self->log->is_trace;
    my $header_value = "amx " . $self->api_key . ':' . $hmac_signature . ':' . $nonce;
    $self->log->trace( "Authorization: $header_value" ) if $self->log->is_trace;
    my $request = HTTP::Request->new( 'POST' => $url );
    $request->header( 'Authorization', $header_value );

Making signature with hamc_sha256(key+method+url+nonce+content) and adding it to request headers.

But OpenApi::Client's pre_process function can't see method and url, so I can't build signature and add it to headers.

return $self->ua->build_tx($http_method, $url, $self->pre_processor->(\%headers, \%req));

How can I make this possible?

basePath and schemes missing from spec

Hi there.

I was trying to use OpenAPI::Client in a project of mine, where I generate the swagger using a plugin for phoenix (the go-to elixir web framework if you are not familiar). It generates swagger like many other plugins i guess, but comes up with swagger thats not quite what OpenAPI::Client expects. More specifically "schemes" and "basePath" is missing from the specification.

When that happens the base_url attribute of OpenAPI::Client gets into trouble with references to undef'ed objects aso.

I found that this version of base_url handled better defaults, to make it survive.

has base_url => sub {
  my $self   = shift;
  my $schema = $self->validator->schema;

  my $scheme = $schema->get('/schemes') || ['http'];

  return Mojo::URL->new->host($schema->get('/host'))->path($schema->get('/basePath') || '/')
    ->scheme($scheme->[0]);
};

I know that you can just brush it of and say: That looks like a generator problem, and you might be right. But no one ever died for having better defaults ;)

OpenAPI::Client rejecting schema accepted by Swagger2::Client

Hi,

Atempting to load schema results in error of:

Invalid JSON specification HASH(0x11c7348):

  • /securityDefinitions/myIdentity: oneOf failed: Properties not allowed: authorizationUrl, flow, scopes, tokenUrl. Properties not allowed: authorizationUrl, flow, scopes, tokenUrl. Properties not allowed: tokenUrl. Properties not allowed: authorizationUrl. Properties not allowed: authorizationUrl. Not in enum list: accessCode. at /usr/local/share/perl/5.18.2/JSON/Validator.pm line 157.

securityDefinitions in API schema looks like:

  "securityDefinitions": {
    "myIdentity": {
      "flow": "implicit",
      "authorizationUrl": "https://identity.server.local/connect/authorize",
      "tokenUrl": "https://identity.server.local/connect/token",
      "scopes": {
        "myAPI": "The scope needed to access the API"
      },
      "type": "oauth2",
      "description": "My Identity Implicit Grant"
    }
  }

This worked with the Swagger2::Client.

Thanks

Fix OpenAPI::Client so it will use the port from the specification file

Hello! There's something weird going on with ports. According to the Swagger2 spec, I should be able to include a port as part of my host as such:

---
swagger: 2.0
host: api.example.com:3300
basePath: /
paths:
  /foo:
    get:
      operationId: listPets
      responses:
        200:
          description: "TEST"
info:
  title: "TEST"
  version: "foo"

But when I try to hit that endpoint with the client, it's somehow adding ":80" to the host. IE:

➜  ~ cat bug.pl 
#!/usr/bin/env perl
use strict;
use warnings;

use OpenAPI::Client;

my $client = OpenAPI::Client->new('min.swagger');
print $client->listPets({})->result->to_string() . "\n";

➜  ~ MOJO_CLIENT_DEBUG=1 perl bug.pl
-- Blocking request (http://api.example.com:3300/foo)
-- Connect 602f9810529364379e04496e1971cc08 (http://api.example.com:3300:80)
Can't resolve: Name or service not known at bug.pl line 8.

I tried digging into it, but I can't seem to figure out where :80 is being added. Do you have any idea what's going on?

numification instead of boolean interpretation of the "exclusiveMaximum" type restriction

We have an OpenAPI specification that has a response component object with status attribute

        status:
          type: integer
          format: int32
          description: >
            The HTTP status code generated by the origin server for this occurrence
            of the problem.
          minimum: 100
          maximum: 600
          exclusiveMaximum: true
          example: 503

We have used the exclusiveMaximum type restriction. Rendering a response with a status code 400 causes a strange validation error

{errors => [{message => "400 >= maximum(1)",path => "/body/status"}],status => 500}

The 1 in maximum(1) is (with high probability) caused by the numification of JSON::PP::true. This looks like a bug.

application/json on requests

Hello again :)

I have noticed that when i do a POST towards some function, the Content-Type: application/json header is not set. Shouldn't it always be set?

The missing header results in an error from the remote API I am using.

   'headers' => bless( {
                                                                       'headers' => {
                                                                                      'content-length' => [
                                                                                                            83
                                                                                                          ],
                                                                                      'accept-encoding' => [
                                                                                                             'gzip'
                                                                                                           ],
                                                                                      'user-agent' => [
                                                                                                        'Mojo-OpenAPI (Perl)'
                                                                                                      ],
                                                                                      'host' => [
                                                                                                  '<host>:<port>'
                                                                                                ]
                                                                                    }
                                                                     }, 'Mojo::Headers' ),
                                          

If i do a POST on the commandline, i.e.

curl -H "Content-Type: application/json" -X POST -d '{"foo":"bar"}' http://<host>:<port>/api/myfunction

It works.

So at least the service expects the header to be set.

/Jesper

response codes are not validated

I have a jenkins Open API specification. I am sending a Disable a job request

  '/job/{name}/disable':
    post:
      description: Disable a job
      operationId: postJobDisable
      parameters:
        - $ref: '#/components/parameters/jobName'
        - $ref: '#/components/parameters/crumb'
      responses:
        '200':
          description: Successfully disabled the job
        '401':
          $ref: '#/components/responses/unauthorized'
        '403':
          $ref: '#/components/responses/forbidden'
        '404':
          $ref: '#/components/responses/jobNotFound'
      security:
        - jenkins_auth: []
      tags:
        - remoteAccess

using the OpenAPI::Client (version 1.0.3) implementation. Although the jenkins response code is 302 which is not one of the specified response codes (200, 401, 403, 404), the client does not complain (dies) or at least warns the user.

authorization/API keys?

Maybe I'm missing something in the docs but how do you pass API keys and other parameters defined in the security section?

Return rejected Promise rather than croak.

Would it be better to return a rejected Mojo::Promise rather than Carp::croak in call_p?

sub call_p {
my ($self, $op) = (shift, shift);
my $code = $self->can("${op}_p") or Carp::croak('[OpenAPI::Client] No such operationId');
return $self->$code(@_);
}

Changing to this.

my $code = $self->can("${op}_p")
    or return Mojo::Promise->new->reject("[OpenAPI::Client] No such operationId");

Allows the usual

$api->call_p('incorrect?')->then(sub { ... }, sub { $error = shift; })->wait;

Failed test 'no definitions added'

Latest versions of Mojolicious::Plugin::OpenAPI and OpenAPI::Client fails a test...

Adding a dump of return value being tested (line 20 of t/command-local-with-ref.t):

  # failing test
  ok !$oc->validator->schema->get('/definitions'), 'no definitions added';
  warn "*********************\n";
  my $r = $oc->validator->schema->get('/definitions');
  warn Dumper($r);

Test output

t/command-local-with-ref.t .. 1/? 
#   Failed test 'no definitions added'
#   at t/command-local-with-ref.t line 24.
*********************
$VAR1 = {
          'DefaultResponse' => {
                                 'required' => [
                                                 'errors'
                                               ],
                                 'properties' => {
                                                   'errors' => {
                                                                 'items' => {
                                                                              'type' => 'object',
                                                                              'required' => [
                                                                                              'message'
                                                                                            ],
                                                                              'properties' => {
                                                                                                'path' => {
                                                                                                            'type' => 'string'
                                                                                                          },
                                                                                                'message' => {
                                                                                                               'type' => 'string'
                                                                                                             }
                                                                                              }
                                                                            },
                                                                 'type' => 'array'
                                                               }
                                                 },
                                 'type' => 'object'
                               }
        };

```

Tests fail (with newest Mojolicious::Plugin::OpenAPI?)

The test suite fails on some of my smokers:

...
#   Failed test 'valid loginUser'
#   at t/body-validation.t line 30.
#          got: '501'
#     expected: '200'

#   Failed test 'valid return'
#   at t/body-validation.t line 31.
#          got: undef
#     expected: '[email protected]'

#   Failed test 'only sent data to server once'
#   at t/body-validation.t line 42.
#          got: '0'
#     expected: '1'
# Looks like you failed 3 tests of 10.
t/body-validation.t ......... 
Dubious, test returned 3 (wstat 768, 0x300)
Failed 3/10 subtests 
... (etc) ...

This seems to happen if Mojolicious::Plugin::OpenAPI 2.x is installed:

****************************************************************
Regression 'mod:Mojolicious::Plugin::OpenAPI'
****************************************************************
Name           	       Theta	      StdErr	 T-stat
[0='const']    	      1.0000	      0.0000	5791088394397843.00
[1='eq_1.26']  	      0.0000	      0.0000	   1.36
[2='eq_1.28']  	      0.0000	      0.0000	   1.11
[3='eq_1.30']  	      0.0000	      0.0000	   1.27
[4='eq_2.00']  	     -1.0000	      0.0000	-5752862970752844.00

R^2= 1.000, N= 119, K= 5
****************************************************************

"array" type query params not respecting API doc

Right now, query params are added to the URL object as an arrayref, which causes them to be rendered as repeated key/vals

query => sub {
my $name = shift;
my $value = _param_as_array($name => $params);
$url->query->param($name => $value);
return {exists => !!@$value, value => $value};
},

That means that if we have a query param called "things":

call({ things => [ qw/one two three/ ] })

It gets rendered to the URL as ?things=one&things=two&things=three.

This is not the correct behavior. For OpenAPI v2, for example, the default (as per the docs) is to connect them via commas, i.e. ?things=one,two,three.

I would love to write up a patch for this, I would just need some guidance as to how to get the correct format from the OpenAPI spec

OpenAPI::Client - how to pass additionalProperties in OpenAPI GET query request?

I'm trying to pass filter variables in a GET request to an OpenAPI route with this specification:

- in: query
   name: params
   schema:
     type: object
     additionalProperties:
       type: string
   style: form
   explode: true       

I've tried various different ways of passing the data at request time:


my $client = OpenAPI::Client->new(...);

my $tx = $client->findApps({
    params => { 'id' =>  '1' }
});

my $tx = $client->findApps({ 'id' =>  '1' });

What is the correct way to do this?

I'm currently getting this warning/error:

Use of uninitialized value $name in exists at /usr/local/share/perl/5.26.1/OpenAPI/Client.pm line 204

openapi /api command throws validation error with basic API

Versions:

  • OpenAPI::Client 0.24
  • JSON::Validator 3.24
  • YAML::XS 0.81
  • Mojolicious 8.12
  • Proforma pet-store OpenAPI specification

Issue:

OpenAPI command does not work with specified pet store. Issue #8 said probably fixed in 0.24 but it is not fixed.

Example:

REST server code actually works and can be called from a REST call:

$ ./app get /api/pets

[2020-03-08 13:14:54.65099] [28570] [debug] GET "/api/pets" (9245ecb7)
[2020-03-08 13:14:55.44719] [28570] [debug] Routing to controller "App::Controller::Pet" and action "list"
[2020-03-08 13:14:55.46035] [28570] [debug] 200 OK (0.809346s, 1.236/s)
{ ... JSON I returned from the test function "list" .. }

OpenAPI client command cannot be executed due to validation issue:

$ app openapi /api

[2020-03-08 13:14:01.41765] [28565] [debug] GET "/api" (3635cea4)
[2020-03-08 13:14:01.41832] [28565] [debug] Routing to a callback
[2020-03-08 13:14:01.41971] [28565] [debug] 200 OK (0.002047s, 488.520/s)
Invalid JSON specification HASH(0x5571d8363290):
- /: Properties not allowed: components, openapi, servers. at /usr/local/share/perl/5.28.1/JSON/Validator.pm line 159.
	JSON::Validator::load_and_validate_schema(JSON::Validator::OpenAPI::Mojolicious=HASH(0x5571d831bc98), "/api", HASH(0x5571d831bf08)) called at /usr/local/share/perl/5.28.1/JSON/Validator/OpenAPI/Mojolicious.pm line 63
	JSON::Validator::OpenAPI::Mojolicious::load_and_validate_schema(JSON::Validator::OpenAPI::Mojolicious=HASH(0x5571d831bc98), "/api", HASH(0x5571d831bf08)) called at /usr/local/share/perl/5.28.1/OpenAPI/Client.pm line 52
	OpenAPI::Client::new("OpenAPI::Client", "/api", "app",App=HASH(0x5571d5e201b0)) called at /usr/local/share/perl/5.28.1/Mojolicious/Command/openapi.pm line 57
	Mojolicious::Command::openapi::run(Mojolicious::Command::openapi=HASH(0x5571d82e0ff8), "/api") called at /usr/share/perl5/Mojolicious/Commands.pm line 55
	Mojolicious::Commands::run(Mojolicious::Commands=HASH(0x5571d83057f0), "openapi", "/api") called at /usr/share/perl5/Mojolicious.pm line 195
	Mojolicious::start(App=HASH(0x5571d5e201b0)) called at /usr/share/perl5/Mojolicious/Commands.pm line 72
	Mojolicious::Commands::start_app("Mojolicious::Commands", "App") called at ./app line 17

$ cat petstore.json

{
  "openapi": "3.0.2",
  "info": {
    "version": "1.0",
    "title": "Some awesome API"
  },
  "paths": {
    "/pets": {
      "get": {
        "operationId": "getPets",
        "x-mojo-name": "get_pets",
        "x-mojo-to": "pet#list",
        "summary": "Finds pets in the system",
        "parameters": [
          {
            "in": "query",
            "name": "age",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Pet response",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "pets": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    }
  },
  "servers": [
    {
      "url": "/api"
    }
  ]
}

Allow passing server uri to constructor

It'd be really nice to support passing the server's URI to the constructor. As it stands it's pretty challenging to use this code against a test server running locally. I have to munge the swagger file to inject the proper host into it.

base_url for openapi v3 services

Hello,

I'm running a local dev server on port 3000 with an openapi v3 spec. OpenAPI::Client was attempting to connect to localhost:80

The problem seems to strive in the fact that openapi v3 doesn't have basePath or host attributes, so OpenAPI::Client is defaulting to http://localhost, instead of picking up the info from the servers array.

I've "fixed" this problem for my specific use case with the following:

has base_url => sub {
  my $self    = shift;
  my $schema  = $self->validator;
  my $schemes = $schema->get('/schemes') || [];

  return Mojo::URL->new($schema->get('/servers/0/url'));

Tests fail (with newest JSON::Validator and/or Mojolicious::Plugin::OpenAPI?)

t/command-local-with-ref.t fails:

[2019-05-05 10:15:58.60959] [9463] [debug] GET "/api" (f832912d)
[2019-05-05 10:15:58.61073] [9463] [debug] Routing to a callback
[2019-05-05 10:15:58.61165] [9463] [debug] 200 OK (0.002408s, 415.282/s)

#   Failed test 'no definitions added'
#   at t/command-local-with-ref.t line 20.
[2019-05-05 10:15:58.68958] [9463] [debug] GET "/ext" (8b70be07)
[2019-05-05 10:15:58.69027] [9463] [debug] Routing to a callback
[2019-05-05 10:15:58.69130] [9463] [debug] 200 OK (0.002s, 500.000/s)
t/command-local-with-ref.t .. skipped: Invalid JSON specification HASH(0x498726a8):
...
Test Summary Report
-------------------
t/command-local-with-ref.t (Wstat: 256 Tests: 3 Failed: 1)
  Failed test:  2
  Non-zero exit status: 1
  Parse errors: Bad plan.  You planned 0 tests but ran 3.

Statistical analysis has two candidates:

****************************************************************
Regression 'mod:Mojolicious::Plugin::OpenAPI'
****************************************************************
Name           	       Theta	      StdErr	 T-stat
[0='const']    	      1.0000	      0.0000	54064544849289584.00
[1='eq_2.14']  	     -1.0000	      0.0000	-52858492505163784.00

R^2= 1.000, N= 68, K= 2
****************************************************************

****************************************************************
Regression 'mod:JSON::Validator'
****************************************************************
Name           	       Theta	      StdErr	 T-stat
[0='const']    	      1.0000	      0.0000	88189917184593344.00
[1='eq_3.09']  	     -1.0000	      0.0000	-86192905442162752.00
[2='eq_3.10']  	     -1.0000	      0.0000	-44094958592296672.00

R^2= 1.000, N= 68, K= 3
****************************************************************

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.