Repository: postmanlabs/postman-code-generators
Pull request #306 (2020-07-13 20:11:55)

Add support to Dart http client
![image](https://user-images.githubusercontent.com/27139702/87349414-7e569f00-c56b-11ea-8279-c075073d4d76.png)
-----
### Feature:
This is the [Dart language](https://dart.dev/) plugin that has support from the [http](https://pub.dev/packages/http) client.

### Related issues:
Dart code generation #231
dart support in code generator #157



Patch:
diff --git a/README.md b/README.md
index f4beebd..ac32c2e 100644
--- a/README.md
+++ b/README.md
@@ -20,6 +20,7 @@ List of supported code generators:
 | C | libcurl |
 | C# | RestSharp | 
 | cURL | cURL | 
+| Dart | http | 
 | Go | Native | 
 | HTTP | HTTP | 
 | Java | OkHttp |
diff --git a/codegens/dart-http/.gitignore b/codegens/dart-http/.gitignore
new file mode 100644
index 0000000..34f4d89
--- /dev/null
+++ b/codegens/dart-http/.gitignore
@@ -0,0 +1,50 @@
+.DS_Store
+
+#Obj-c files
+*.m
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+
+# Coverage directory used by tools like istanbul
+.coverage
+
+# node-waf configuration
+.lock-wscript
+
+
+# Dependency directories
+node_modules/
+jspm_packages/
+
+# Typescript v1 declaration files
+typings/
+
+# Optional npm cache directory
+.npm
+
+# Optional eslint cache
+.eslintcache
+
+# Optional REPL history
+.node_repl_history
+
+# Output of 'npm pack'
+*.tgz
+
+# Yarn Integrity file
+.yarn-integrity
+
+# dotenv environment variables file
+.env
+
+out/
+/.idea/
+pubspec.lock
+pubspec.yaml
+.packages
+snippet.dart
+.dart_tool/
diff --git a/codegens/dart-http/.npmignore b/codegens/dart-http/.npmignore
new file mode 100644
index 0000000..79ad2ba
--- /dev/null
+++ b/codegens/dart-http/.npmignore
@@ -0,0 +1,76 @@
+### NPM Specific: Disregard recursive project files
+### ===============================================
+/.editorconfig
+/.gitmodules
+/test
+
+### Borrowed from .gitignore
+### ========================
+
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+
+# Runtime data
+pids
+*.pid
+*.seed
+*.pid.lock
+
+# Prevent IDE stuff
+.idea
+.vscode
+*.sublime-*
+
+# Directory for instrumented libs generated by jscoverage/JSCover
+lib-cov
+
+# Coverage directory used by tools like istanbul
+.coverage
+
+# nyc test coverage
+.nyc_output
+
+# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
+.grunt
+
+# Bower dependency directory (https://bower.io/)
+bower_components
+
+# node-waf configuration
+.lock-wscript
+
+# Compiled binary addons (http://nodejs.org/api/addons.html)
+build/Release
+
+# Dependency directories
+node_modules/
+jspm_packages/
+
+# Typescript v1 declaration files
+typings/
+
+# Optional npm cache directory
+.npm
+
+# Optional eslint cache
+.eslintcache
+
+# Optional REPL history
+.node_repl_history
+
+# Output of 'npm pack'
+*.tgz
+
+# Yarn Integrity file
+.yarn-integrity
+
+# dotenv environment variables file
+.env
+
+snippet.swift
+
+out/
diff --git a/codegens/dart-http/README.md b/codegens/dart-http/README.md
new file mode 100644
index 0000000..b4bf996
--- /dev/null
+++ b/codegens/dart-http/README.md
@@ -0,0 +1,42 @@
+
+> Converts Postman-SDK Request into code snippet for Dart http.
+
+#### Prerequisites
+To run Code-Gen, ensure that you have NodeJS >= v8. A copy of the NodeJS installable can be downloaded from https://nodejs.org/en/download/package-manager.
+
+## Using the Module
+The module will expose an object which will have property `convert` which is the function for converting the Postman-SDK request to Dart code snippet.
+
+### convert function
+Convert function takes three parameters
+
+* `request` - Postman-SDK Request Object
+
+* `options` - options is an object which hsa following properties
+    * `indentType` - String denoting type of indentation for code snippet. eg: 'Space', 'Tab'
+    * `indentCount` - The number of indentation characters to add per code level
+    * `trimRequestBody` - Whether or not request body fields should be trimmed
+    * `includeBoilerplate` - Include class definition and import statements in snippet
+    * `followRedirect` - Automatically follow HTTP redirects
+
+* `callback` - callback function with first parameter as error and second parameter as string for code snippet
+
+##### Example:
+```js
+var request = new sdk.Request('www.google.com'),  //using postman sdk to create request  
+    options = {
+        indentCount: 3,
+        indentType: 'Space',
+        requestTimeout: 200,
+        trimRequestBody: true
+    };
+convert(request, options, function(error, snippet) {
+    if (error) {
+        //  handle error
+    }
+    //  handle snippet
+});
+```
+### Guidelines for using generated snippet
+
+* This module doesn't support cookies.
diff --git a/codegens/dart-http/index.js b/codegens/dart-http/index.js
new file mode 100644
index 0000000..bb0a047
--- /dev/null
+++ b/codegens/dart-http/index.js
@@ -0,0 +1,1 @@
+module.exports = require('./lib');
diff --git a/codegens/dart-http/lib/index.js b/codegens/dart-http/lib/index.js
new file mode 100644
index 0000000..0f4469e
--- /dev/null
+++ b/codegens/dart-http/lib/index.js
@@ -0,0 +1,336 @@
+var _ = require('./lodash'),
+  sanitizeOptions = require('./util').sanitizeOptions,
+  sanitize = require('./util').sanitize,
+  addFormParam = require('./util').addFormParam,
+  self;
+
+/**
+ * Parses Url encoded data
+ *
+ * @param {Object} body body data
+ * @param {String} indent indentation required for code snippet
+ * @param {Boolean} trim indicates whether to trim string or not
+ */
+function parseUrlEncoded (body, indent, trim) {
+  var bodySnippet = 'request.bodyFields = {',
+    enabledBodyList = _.reject(body, 'disabled'),
+    bodyDataMap;
+  if (!_.isEmpty(enabledBodyList)) {
+    bodyDataMap = _.map(enabledBodyList, function (value) {
+      return `${indent}'${sanitize(value.key, trim)}': '${sanitize(value.value, trim)}'`;
+    });
+    bodySnippet += '\n' + bodyDataMap.join(',\n') + '\n';
+  }
+  bodySnippet += '};';
+  return bodySnippet;
+}
+
+/**
+ * Parses Raw data
+ *
+ * @param {Object} body Raw body data
+ * @param {Boolean} trim indicates whether to trim string or not
+ */
+function parseRawBody (body, trim) {
+  return `request.body = '''${sanitize(body, trim)}''';`;
+}
+
+/**
+ * Parses GraphQL body
+ *
+ * @param {Object} body GraphQL body
+ * @param {Boolean} trim indicates whether to trim string or not
+ */
+function parseGraphQLBody (body, trim) {
+  var bodySnippet = '',
+    query = body.query,
+    graphqlVariables;
+  try {
+    graphqlVariables = JSON.parse(body.variables);
+  }
+  catch (e) {
+    graphqlVariables = {};
+  }
+
+  bodySnippet += `request.body = '''${sanitize(JSON.stringify({
+    query: query,
+    variables: graphqlVariables
+  }), trim)}''';\n`;
+
+  return bodySnippet;
+}
+
+/**
+ * Parses form data body from request
+ *
+ * @param {Object} body form data Body
+ * @param {String} indent indentation required for code snippet
+ * @param {Boolean} trim indicates whether to trim string or not
+ */
+function parseFormData (body, indent, trim) {
+  let bodySnippet = '',
+    formDataArray = [],
+    formDataFileArray = [],
+    key,
+    value;
+
+  if (_.isEmpty(body)) {
+    return bodySnippet;
+  }
+
+  _.forEach(body, function (data) {
+    key = trim ? data.key.trim() : data.key;
+    value = trim ? data.value.trim() : data.value;
+    if (!data.disabled) {
+      if (data.type === 'file') {
+        formDataFileArray.push(`request.files.add(await http.MultipartFile.fromPath('${key}', '${data.src}'));`);
+      }
+      else {
+        formDataArray.push(`${indent}'${sanitize(key)}': '${sanitize(value, trim)}'`);
+      }
+    }
+  });
+
+  if (formDataArray.length > 0) {
+    bodySnippet += 'request.fields.addAll({\n';
+    bodySnippet += formDataArray.join(',\n');
+    bodySnippet += '\n});\n';
+  }
+
+  if (formDataFileArray.length > 0) {
+    bodySnippet += formDataFileArray.join('\n');
+  }
+
+  return bodySnippet;
+}
+
+/**
+ * Parses Body from the Request
+ *
+ * @param {Object} body body object from request.
+ * @param {String} indent indentation required for code snippet
+ * @param {trim} trim indicates whether to trim string or not
+ */
+function parseBody (body, indent, trim) {
+  if (!_.isEmpty(body)) {
+    switch (body.mode) {
+      case 'urlencoded':
+        return parseUrlEncoded(body.urlencoded, indent, trim);
+      case 'raw':
+        return parseRawBody(body.raw, trim);
+      case 'formdata':
+        return parseFormData(body.formdata, indent, trim);
+      case 'graphql':
+        return parseGraphQLBody(body.graphql, trim);
+      case 'file':
+        return 'request.body = r\'<file contents here>\';\n';
+      default:
+        return '';
+    }
+  }
+  return '';
+}
+
+/**
+ * Parses headers from the request.
+ *
+ * @param {Object} headersArray array containing headers
+ * @param {String} indent indentation required for code snippet
+ * @param {Boolean} trim indicates whether to trim string or not
+ */
+function parseHeaders (headersArray, indent, trim) {
+  var headerString = '',
+    headerDictionary = [];
+  if (_.isEmpty(headersArray)) {
+    return headerString;
+  }
+
+  headerString += 'var headers = {\n';
+
+  _.forEach(headersArray, function (header) {
+    if (!header.disabled) {
+      headerDictionary.push(indent + '\'' + header.key + '\': \'' + sanitize(header.value, trim) + '\'');
+    }
+  });
+
+  headerString += headerDictionary.join(',\n');
+  headerString += '\n};\n';
+
+  return headerString;
+}
+
+self = module.exports = {
+  convert: function (request, options, callback) {
+    var indent,
+      codeSnippet = '',
+      headerSnippet = '',
+      footerSnippet = '',
+      trim,
+      timeout,
+      followRedirect;
+    options = sanitizeOptions(options, self.getOptions());
+    if (options.includeBoilerplate) {
+      headerSnippet = 'import \'package:http/http.dart\' as http;\n\n';
+      headerSnippet += 'void main() async {\n';
+      footerSnippet = '}\n';
+    }
+    trim = options.trimRequestBody;
+    indent = options.indentType === 'Tab' ? '\t' : ' ';
+    indent = indent.repeat(options.indentCount);
+    timeout = options.requestTimeout;
+    followRedirect = options.followRedirect;
+
+    if (!_.isFunction(callback)) {
+      throw new Error('Callback is not valid function');
+    }
+
+    if (request.body && !request.headers.has('Content-Type')) {
+      if (request.body.mode === 'file') {
+        request.addHeader({
+          key: 'Content-Type',
+          value: 'text/plain'
+        });
+      }
+      else if (request.body.mode === 'graphql') {
+        request.addHeader({
+          key: 'Content-Type',
+          value: 'application/json'
+        });
+      }
+    }
+
+    // The following code handles multiple files in the same formdata param.
+    // It removes the form data params where the src property is an array of filepath strings
+    // Splits that array into different form data params with src set as a single filepath string
+    if (request.body && request.body.mode === 'formdata') {
+      let formdata = request.body.formdata,
+        formdataArray = [];
+      formdata.members.forEach((param) => {
+        let key = param.key,
+          type = param.type,
+          disabled = param.disabled,
+          contentType = param.contentType;
+        // check if type is file or text
+        if (type === 'file') {
+          // if src is not of type string we check for array(multiple files)
+          if (typeof param.src !== 'string') {
+            // if src is an array(not empty), iterate over it and add files as separate form fields
+            if (Array.isArray(param.src) && param.src.length) {
+              param.src.forEach((filePath) => {
+                addFormParam(formdataArray, key, param.type, filePath, disabled, contentType);
+              });
+            }
+            // if src is not an array or string, or is an empty array, add a placeholder for file path(no files case)
+            else {
+              addFormParam(formdataArray, key, param.type, '/path/to/file', disabled, contentType);
+            }
+          }
+          // if src is string, directly add the param with src as filepath
+          else {
+            addFormParam(formdataArray, key, param.type, param.src, disabled, contentType);
+          }
+        }
+        // if type is text, directly add it to formdata array
+        else {
+          addFormParam(formdataArray, key, param.type, param.value, disabled, contentType);
+        }
+      });
+      request.body.update({
+        mode: 'formdata',
+        formdata: formdataArray
+      });
+    }
+
+    const headers = parseHeaders(request.headers.toJSON(), indent, trim),
+      requestBody = request.body ? request.body.toJSON() : {},
+      body = parseBody(requestBody, indent, trim) + '\n';
+
+    codeSnippet += headers;
+
+    if (requestBody && requestBody.mode === 'formdata') {
+      codeSnippet += `var request = http.MultipartRequest('${request.method.toUpperCase()}',` +
+        ` Uri.parse('${request.url.toString()}'));\n`;
+    }
+    else {
+      codeSnippet += `var request = http.Request('${request.method.toUpperCase()}',` +
+      ` Uri.parse('${request.url.toString()}'));\n`;
+    }
+
+    if (body !== '') {
+      codeSnippet += body;
+    }
+    if (headers !== '') {
+      codeSnippet += 'request.headers.addAll(headers);\n';
+    }
+    if (!followRedirect) {
+      codeSnippet += 'request.followRedirects = false;\n';
+    }
+
+    codeSnippet += '\n';
+
+    codeSnippet += 'http.StreamedResponse response = await request.send()';
+    if (timeout > 0) {
+      codeSnippet += `.timeout(Duration(milliseconds: ${timeout}))`;
+    }
+    codeSnippet += ';\n';
+    codeSnippet += 'if (response.statusCode == 200) {\n';
+    codeSnippet += `${indent}print(await response.stream.bytesToString());\n`;
+    codeSnippet += '} else {\n';
+    codeSnippet += `${indent}print(response.reasonPhrase);\n`;
+    codeSnippet += '}\n';
+
+    //  if boilerplate is included then two more indent needs to be added in snippet
+    (options.includeBoilerplate) &&
+    (codeSnippet = indent + codeSnippet.split('\n').join('\n' + indent) + '\n');
+
+    callback(null, headerSnippet + codeSnippet + footerSnippet);
+  },
+  getOptions: function () {
+    return [
+      {
+        name: 'Set indentation count',
+        id: 'indentCount',
+        type: 'positiveInteger',
+        default: 2,
+        description: 'Set the number of indentation characters to add per code level'
+      },
+      {
+        name: 'Set indentation type',
+        id: 'indentType',
+        type: 'enum',
+        availableOptions: ['Tab', 'Space'],
+        default: 'Space',
+        description: 'Select the character used to indent lines of code'
+      },
+      {
+        name: 'Set request timeout',
+        id: 'requestTimeout',
+        type: 'positiveInteger',
+        default: 0,
+        description: 'Set number of milliseconds the request should wait for a response' +
+    ' before timing out (use 0 for infinity)'
+      },
+      {
+        name: 'Trim request body fields',
+        id: 'trimRequestBody',
+        type: 'boolean',
+        default: false,
+        description: 'Remove white space and additional lines that may affect the server\'s response'
+      },
+      {
+        name: 'Include boilerplate',
+        id: 'includeBoilerplate',
+        type: 'boolean',
+        default: false,
+        description: 'Include class definition and import statements in snippet'
+      },
+      {
+        name: 'Follow redirects',
+        id: 'followRedirect',
+        type: 'boolean',
+        default: true,
+        description: 'Automatically follow HTTP redirects'
+      }
+    ];
+  }
+};
diff --git a/codegens/dart-http/lib/lodash.js b/codegens/dart-http/lib/lodash.js
new file mode 100644
index 0000000..5be147a
--- /dev/null
+++ b/codegens/dart-http/lib/lodash.js
@@ -0,0 +1,455 @@
+/* istanbul ignore next */
+module.exports = {
+
+  /**
+       * Checks if `value` is an empty object, array or string.
+       *
+       * Objects are considered empty if they have no own enumerable string keyed
+       * properties.
+       *
+       * Values such as strings, arrays are considered empty if they have a `length` of `0`.
+       *
+       * @param {*} value The value to check.
+       * @returns {boolean} Returns `true` if `value` is empty, else `false`.
+       * @example
+       *
+       * isEmpty(null)
+       * // => true
+       *
+       * isEmpty(true)
+       * // => true
+       *
+       * isEmpty(1)
+       * // => true
+       *
+       * isEmpty([1, 2, 3])
+       * // => false
+       *
+       * isEmpty('abc')
+       * // => false
+       *
+       * isEmpty({ 'a': 1 })
+       * // => false
+       */
+  isEmpty: function (value) {
+    // eslint-disable-next-line lodash/prefer-is-nil
+    if (value === null || value === undefined) {
+      return true;
+    }
+    if (Array.isArray(value) || typeof value === 'string' || typeof value.splice === 'function') {
+      return !value.length;
+    }
+
+    for (const key in value) {
+      if (Object.prototype.hasOwnProperty.call(value, key)) {
+        return false;
+      }
+    }
+    return true;
+  },
+
+  /**
+       * Checks if `value` is `undefined`.
+       *
+       * @param {*} value The value to check.
+       * @returns {boolean} Returns `true` if `value` is `undefined`, else `false`.
+       * @example
+       *
+       * isUndefined(void 0)
+       * // => true
+       *
+       * isUndefined(null)
+       * // => false
+       */
+  isUndefined: function (value) {
+    return value === undefined;
+  },
+
+  /**
+       * Checks if `func` is classified as a `Function` object.
+       *
+       * @param {*} func The value to check.
+       * @returns {boolean} Returns `true` if `func` is a function, else `false`.
+       * @example
+       *
+       * isFunction(self.isEmpty)
+       * // => true
+       *
+       * isFunction(/abc/)
+       * // => false
+       */
+  isFunction: function (func) {
+    return typeof func === 'function';
+  },
+
+  /**
+       * Converts the first character of `string` to upper case and the remaining
+       * to lower case.
+       *
+       * @param {string} [string=''] The string to capitalize.
+       * @returns {string} Returns the capitalized string.
+       * @example
+       *
+       * capitalize('FRED')
+       * // => 'Fred'
+       *
+       * capitalize('john')
+       * // => 'John'
+       */
+
+  capitalize: function (string) {
+    return string.charAt(0).toUpperCase() + string.slice(1).toLowerCase();
+  },
+
+  /**
+       * Reduces `array` to a value which is the accumulated result of running
+       * each element in `array` thru `iteratee`, where each successive
+       * invocation is supplied the return value of the previous. If `accumulator`
+       * is not given, the first element of `array` is used as the initial
+       * value. The iteratee is invoked with four arguments:
+       * (accumulator, value, index|key, array).
+       *
+       * @param {Array} array The Array to iterate over.
+       * @param {Function} iteratee The function invoked per iteration.
+       * @param {*} [accumulator] The initial value.
+       * @returns {*} Returns the accumulated value.
+       * @example
+       *
+       * reduce([1, 2], (sum, n) => sum + n, 0)
+       * // => 3
+       *
+       */
+  reduce: function (array, iteratee, accumulator) {
+    return array.reduce(iteratee, accumulator);
+  },
+
+  /**
+       * Iterates over elements of `array`, returning an array of all elements
+       * `predicate` returns truthy for. The predicate is invoked with three
+       * arguments: (value, index, array).
+       *
+       * @param {Array} array The array to iterate over.
+       * @param {Function|object} predicate The function/object invoked per iteration.
+       * @returns {Array} Returns the new filtered array.
+       * @example
+       *
+       * const users = [
+          *   { 'user': 'barney', 'active': true },
+          *   { 'user': 'fred',   'active': false }
+          * ]
+          *
+          * filter(users, ({ active }) => active)
+          * // => object for ['barney']
+          */
+  filter: function (array, predicate) {
+    if (typeof predicate === 'function') {
+      return array.filter(predicate);
+    }
+    var key = Object.keys(predicate),
+      val = predicate[key],
+      res = [];
+    array.forEach(function (item) {
+      if (item[key] && item[key] === val) {
+        res.push(item);
+      }
+    });
+    return res;
+  },
+
+  /**
+       * The opposite of `filter` this method returns the elements of `array`
+       * that `predicate` does **not** return truthy for.
+       *
+       * @param {Array} array collection to iterate over.
+       * @param {String} predicate The String that needs to have truthy value, invoked per iteration.
+       * @returns {Array} Returns the new filtered array.
+       * @example
+       *
+       * const users = [
+          *   { 'user': 'barney', 'active': true },
+          *   { 'user': 'fred',   'active': false }
+          * ]
+          *
+          * reject(users, 'active')
+          * // => object for ['fred']
+          */
+  reject: function (array, predicate) {
+    var res = [];
+    array.forEach((object) => {
+      if (!object[predicate]) {
+        res.push(object);
+      }
+    });
+    return res;
+  },
+
+  /**
+       * Creates an array of values by running each element of `array` thru `iteratee`.
+       * The iteratee is invoked with three arguments: (value, index, array).
+       *
+       * @param {Array} array The array to iterate over.
+       * @param {Function} iteratee The function invoked per iteration.
+       * @returns {Array} Returns the new mapped array.
+       * @example
+       *
+       * function square(n) {
+          *   return n * n
+          * }
+          *
+          * map([4, 8], square)
+          * // => [16, 64]
+          */
+  map: function (array, iteratee) {
+    return array.map(iteratee);
+  },
+
+  /**
+       * Iterates over elements of `collection` and invokes `iteratee` for each element.
+       * The iteratee is invoked with three arguments: (value, index|key, collection).
+       *
+       * @param {Array|Object} collection The collection to iterate over.
+       * @param {Function} iteratee The function invoked per iteration.
+       * @returns {Array|Object} Returns `collection`.
+       * @example
+       *
+       * forEach([1, 2], value => console.log(value))
+       * // => Logs `1` then `2`.
+       *
+       * forEach({ 'a': 1, 'b': 2 }, (value, key) => console.log(key))
+       * // => Logs 'a' then 'b'
+       */
+
+  forEach: function (collection, iteratee) {
+    if (collection === null) {
+      return null;
+    }
+
+    if (Array.isArray(collection)) {
+      return collection.forEach(iteratee);
+    }
+    const iterable = Object(collection),
+      props = Object.keys(collection);
+    var index = -1,
+      key, i;
+
+    for (i = 0; i < props.length; i++) {
+      key = props[++index];
+      iteratee(iterable[key], key, iterable);
+    }
+    return collection;
+  },
+
+  /**
+       * Checks if `value` is in `collection`. If `collection` is a string, it's
+       * checked for a substring of `value`, otherwise it checks if the `value` is present
+       * as a key in a `collection` object.
+       *
+       * @param {Array|Object|string} collection The collection to inspect.
+       * @param {*} value The value to search for.
+       * @returns {boolean} Returns `true` if `value` is found, else `false`.
+       * @example
+       *
+       * _.includes([1, 2, 3], 1);
+       * // => true
+       *
+       * _.includes({ 'a': 1, 'b': 2 }, 1);
+       * // => true
+       *
+       * _.includes('abcd', 'bc');
+       * // => true
+       */
+  includes: function (collection, value) {
+    if (Array.isArray(collection) || typeof collection === 'string') {
+      return collection.includes(value);
+    }
+    for (var key in collection) {
+      if (collection.hasOwnProperty(key)) {
+        if (collection[key] === value) {
+          return true;
+        }
+      }
+    }
+    return false;
+  },
+
+  /**
+       * Gets the size of `collection` by returning its length for array and strings.
+       * For objects it returns the number of enumerable string keyed
+       * properties.
+       *
+       * @param {Array|Object|string} collection The collection to inspect.
+       * @returns {number} Returns the collection size.
+       * @example
+       *
+       * size([1, 2, 3])
+       * // => 3
+       *
+       * size({ 'a': 1, 'b': 2 })
+       * // => 2
+       *
+       * size('pebbles')
+       * // => 7
+       */
+  size: function (collection) {
+    // eslint-disable-next-line lodash/prefer-is-nil
+    if (collection === null || collection === undefined) {
+      return 0;
+    }
+    if (Array.isArray(collection) || typeof collection === 'string') {
+      return collection.length;
+    }
+
+    return Object.keys(collection).length;
+  },
+
+  /**
+       * Converts all elements in `array` into a string separated by `separator`.
+       *
+       * @param {Array} array The array to convert.
+       * @param {string} [separator=','] The element separator.
+       * @returns {string} Returns the joined string.
+       * @example
+       *
+       * _.join(['a', 'b', 'c'], '~');
+       * // => 'a~b~c'
+       */
+  join: function (array, separator) {
+    if (array === null) {
+      return '';
+    }
+    return array.join(separator);
+  },
+
+  /**
+       * Removes trailing whitespace or specified characters from `string`.
+       *
+       * @param {string} [string=''] The string to trim.
+       * @param {string} [chars=whitespace] The characters to trim.
+       * @returns {string} Returns the trimmed string.
+       * @example
+       *
+       * trimEnd('  abc  ')
+       * // => '  abc'
+       *
+       * trimEnd('-_-abc-_-', '_-')
+       * // => '-_-abc'
+       */
+  trimEnd: function (string, chars) {
+    if (!string) {
+      return '';
+    }
+    if (string && !chars) {
+      return string.replace(/\s*$/, '');
+    }
+    chars += '$';
+    return string.replace(new RegExp(chars, 'g'), '');
+  },
+
+  /**
+       * Returns the index of the first
+       * element `predicate` returns truthy for.
+       *
+       * @param {Array} array The array to inspect.
+       * @param {Object} predicate The exact object to be searched for in the array.
+       * @returns {number} Returns the index of the found element, else `-1`.
+       * @example
+       *
+       * var users = [
+          *   { 'user': 'barney',  'active': false },
+          *   { 'user': 'fred',    'active': false },
+          *   { 'user': 'pebbles', 'active': true }
+          * ];
+          *
+          * _.findIndex(users, { 'user': 'fred', 'active': false });
+          * // => 1
+          *
+          * _.findIndex(users, {'active' : false});
+          * // => 0
+          *
+          */
+  findIndex: function (array, predicate) {
+    var length = array === null ? 0 : array.length,
+      index = -1,
+      keys = Object.keys(predicate),
+      found, i;
+    if (!length) {
+      return -1;
+    }
+    for (i = 0; i < array.length; i++) {
+      found = true;
+      // eslint-disable-next-line no-loop-func
+      keys.forEach((key) => {
+        if (!(array[i][key] && array[i][key] === predicate[key])) {
+          found = false;
+        }
+      });
+      if (found) {
+        index = i;
+        break;
+      }
+    }
+    return index;
+  },
+
+  /**
+       * Gets the value at `path` of `object`. If the resolved value is
+       * `undefined`, the `defaultValue` is returned in its place.
+       *
+       * @param {Object} object The object to query.
+       * @param {string} path The path of the property to get.
+       * @param {*} [defaultValue] The value returned for `undefined` resolved values.
+       * @returns {*} Returns the resolved value.
+       * @example
+       *
+       * const object = { a: {b : 'c'} }
+       *
+       *
+       * get(object, 'a.b.c', 'default')
+       * // => 'default'
+       *
+       * get(object, 'a.b', 'default')
+       * // => 'c'
+       */
+  get: function (object, path, defaultValue) {
+    if (object === null) {
+      return undefined;
+    }
+    var arr = path.split('.'),
+      res = object,
+      i;
+    for (i = 0; i < arr.length; i++) {
+      res = res[arr[i]];
+      if (res === undefined) {
+        return defaultValue;
+      }
+    }
+    return res;
+  },
+
+  /**
+       * Checks if `predicate` returns truthy for **all** elements of `array`.
+       * Iteration is stopped once `predicate` returns falsey. The predicate is
+       * invoked with three arguments: (value, index, array).
+       *
+       * @param {Array} array The array to iterate over.
+       * @param {Function} predicate The function invoked per iteration.
+       * @returns {boolean} Returns `true` if all elements pass the predicate check,
+       *  else `false`.
+       * @example
+       *
+       * every([true, 1, null, 'yes'], Boolean)
+       * // => false
+       */
+  every: function (array, predicate) {
+    var index = -1,
+      length = array === null ? 0 : array.length;
+
+    while (++index < length) {
+      if (!predicate(array[index], index, array)) {
+        return false;
+      }
+    }
+    return true;
+  }
+
+};
diff --git a/codegens/dart-http/lib/util.js b/codegens/dart-http/lib/util.js
new file mode 100644
index 0000000..4d1801f
--- /dev/null
+++ b/codegens/dart-http/lib/util.js
@@ -0,0 +1,123 @@
+module.exports = {
+  /**
+     * sanitizes input string by handling escape characters eg: converts '''' to '\'\''
+     * and trim input if required
+     *
+     * @param {String} inputString
+     * @param {Boolean} [trim] - indicates whether to trim string or not
+     * @returns {String}
+     */
+  sanitize: function (inputString, trim) {
+    if (typeof inputString !== 'string') {
+      return '';
+    }
+    inputString = inputString.replace(/\\/g, '\\\\')
+      .replace(/\n/g, '\\n')
+      .replace(/\r/g, '\\r')
+      .replace(/\t/g, '\\t')
+      .replace(/'/g, '\\\'')
+      .replace(/\$/g, '\\$');
+    return trim ? inputString.trim() : inputString;
+
+  },
+
+  /**
+    * sanitizes input options
+    *
+    * @param {Object} options - Options provided by the user
+    * @param {Array} optionsArray - options array received from getOptions function
+    *
+    * @returns {Object} - Sanitized options object
+    */
+  sanitizeOptions: function (options, optionsArray) {
+    var result = {},
+      defaultOptions = {},
+      id;
+    optionsArray.forEach((option) => {
+      defaultOptions[option.id] = {
+        default: option.default,
+        type: option.type
+      };
+      if (option.type === 'enum') {
+        defaultOptions[option.id].availableOptions = option.availableOptions;
+      }
+    });
+
+    for (id in options) {
+      if (options.hasOwnProperty(id)) {
+        if (defaultOptions[id] === undefined) {
+          continue;
+        }
+        switch (defaultOptions[id].type) {
+          case 'boolean':
+            if (typeof options[id] !== 'boolean') {
+              result[id] = defaultOptions[id].default;
+            }
+            else {
+              result[id] = options[id];
+            }
+            break;
+          case 'positiveInteger':
+            if (typeof options[id] !== 'number' || options[id] < 0) {
+              result[id] = defaultOptions[id].default;
+            }
+            else {
+              result[id] = options[id];
+            }
+            break;
+          case 'enum':
+            if (!defaultOptions[id].availableOptions.includes(options[id])) {
+              result[id] = defaultOptions[id].default;
+            }
+            else {
+              result[id] = options[id];
+            }
+            break;
+          default:
+            result[id] = options[id];
+        }
+      }
+    }
+
+    for (id in defaultOptions) {
+      if (defaultOptions.hasOwnProperty(id)) {
+        if (result[id] === undefined) {
+          result[id] = defaultOptions[id].default;
+        }
+      }
+    }
+    return result;
+  },
+
+  /**
+   *
+   * @param {Array} array - form data array
+   * @param {String} key - key of form data param
+   * @param {String} type - type of form data param(file/text)
+   * @param {String} val - value/src property of form data param
+   * @param {String} disabled - Boolean denoting whether the param is disabled or not
+   * @param {String} contentType - content type header of the param
+   *
+   * Appends a single param to form data array
+ */
+  addFormParam: function (array, key, type, val, disabled, contentType) {
+    if (type === 'file') {
+      array.push({
+        key: key,
+        type: type,
+        src: val,
+        disabled: disabled,
+        contentType: contentType
+      });
+    }
+    else {
+      array.push({
+        key: key,
+        type: type,
+        value: val,
+        disabled: disabled,
+        contentType: contentType
+      });
+    }
+  }
+};
diff --git a/codegens/dart-http/npm-shrinkwrap.json b/codegens/dart-http/npm-shrinkwrap.json
new file mode 100644
index 0000000..9339a9b
--- /dev/null
+++ b/codegens/dart-http/npm-shrinkwrap.json
@@ -0,0 +1,5 @@
+{
+  "name": "@postman/codegen-dart",
+  "version": "0.0.1",
+  "lockfileVersion": 1
+}
diff --git a/codegens/dart-http/package.json b/codegens/dart-http/package.json
new file mode 100644
index 0000000..c89a4ff
--- /dev/null
+++ b/codegens/dart-http/package.json
@@ -0,0 +1,34 @@
+{
+  "name": "@postman/codegen-dart-http",
+  "version": "0.0.1",
+  "description": "Converts Postman SDK Requests to Dart code snippet",
+  "main": "index.js",
+  "com_postman_plugin": {
+    "type": "code_generator",
+    "lang": "Dart",
+    "variant": "http",
+    "syntax_mode": "dart"
+  },
+  "directories": {
+    "lib": "lib",
+    "test": "test"
+  },
+  "scripts": {
+    "test": "node npm/test.js",
+    "test-lint": "node npm/test-lint.js",
+    "test-unit": "node npm/test-unit.js",
+    "test-newman": "node npm/test-newman.js"
+  },
+  "repository": {
+    "type": "git",
+    "url": ""
+  },
+  "author": "Postman Labs <help@getpostman.com>",
+  "license": "Apache-2.0",
+  "homepage": "https://github.com/postmanlabs/code-generators/tree/master/codegens/dart",
+  "dependencies": {},
+  "devDependencies": {},
+  "engines": {
+    "node": ">=8"
+  }
+}
diff --git a/lib/assets/languageLabels.json b/lib/assets/languageLabels.json
index a426372..9709e33 100644
--- a/lib/assets/languageLabels.json
+++ b/lib/assets/languageLabels.json
@@ -17,5 +17,6 @@
   "csharp": "C#",
   "nodejs": "NodeJs",
   "ocaml": "OCaml",
-  "shell": "Shell"
+  "shell": "Shell",
+  "dart": "Dart"
 }
diff --git a/npm/ci-requirements.sh b/npm/ci-requirements.sh
index c62e395..c0bd77b 100755
--- a/npm/ci-requirements.sh
+++ b/npm/ci-requirements.sh
@@ -46,3 +46,15 @@ sudo apt-get install -y mono-complete
 
 echo "Installing dependencies required for tests in codegens/shell-httpie"
 sudo apt-get install httpie
+
+echo "Installing dependencies required for tests in codegens/dart"
+pushd ./codegens/dart-http &>/dev/null;
+  wget https://storage.googleapis.com/dart-archive/channels/stable/release/2.10.2/linux_packages/dart_2.10.2-1_amd64.deb
+  sudo dpkg -i dart_2.10.2-1_amd64.deb
+  echo '''name: test
+dependencies:
+  http: ^0.12.2
+''' > pubspec.yaml
+  dart pub get
+popd &>/dev/null;
+


Test patch:
diff --git a/codegens/dart-http/npm/test-lint.js b/codegens/dart-http/npm/test-lint.js
new file mode 100644
index 0000000..91bdef1
--- /dev/null
+++ b/codegens/dart-http/npm/test-lint.js
@@ -0,0 +1,57 @@
+#!/usr/bin/env node
+require('shelljs/global');
+
+var chalk = require('chalk'),
+  async = require('async'),
+  ESLintCLIEngine = require('eslint').CLIEngine,
+
+  /**
+     * The list of source code files / directories to be linted.
+     *
+     * @type {Array}
+     */
+  LINT_SOURCE_DIRS = [
+    './lib',
+    './test',
+    './npm/*.js',
+    './index.js'
+  ];
+
+module.exports = function (exit) {
+  // banner line
+  console.info(chalk.yellow.bold('\nLinting files using eslint...'));
+
+  async.waterfall([
+
+    /**
+         * Instantiates an ESLint CLI engine and runs it in the scope defined within LINT_SOURCE_DIRS.
+         *
+         * @param {Function} next - The callback function whose invocation marks the end of the lint test run.
+         * @returns {*}
+         */
+    function (next) {
+      next(null, (new ESLintCLIEngine()).executeOnFiles(LINT_SOURCE_DIRS));
+    },
+
+    /**
+         * Processes a test report from the Lint test runner, and displays meaningful results.
+         *
+         * @param {Object} report - The overall test report for the current lint test.
+         * @param {Object} report.results - The set of test results for the current lint run.
+         * @param {Function} next - The callback whose invocation marks the completion of the post run tasks.
+         * @returns {*}
+         */
+    function (report, next) {
+      var errorReport = ESLintCLIEngine.getErrorResults(report.results);
+      // log the result to CLI
+      console.info(ESLintCLIEngine.getFormatter()(report.results));
+      // log the success of the parser if it has no errors
+      (errorReport && !errorReport.length) && console.info(chalk.green('eslint ok!'));
+      // ensure that the exit code is non zero in case there was an error
+      next(Number(errorReport && errorReport.length) || 0);
+    }
+  ], exit);
+};
+
+// ensure we run this script exports if this is a direct stdin.tty run
+!module.parent && module.exports(exit);
diff --git a/codegens/dart-http/npm/test-newman.js b/codegens/dart-http/npm/test-newman.js
new file mode 100644
index 0000000..e8f9707
--- /dev/null
+++ b/codegens/dart-http/npm/test-newman.js
@@ -0,0 +1,59 @@
+#!/usr/bin/env node
+/* eslint-env node, es6 */
+// ---------------------------------------------------------------------------------------------------------------------
+// This script is intended to execute all newman tests.
+// ---------------------------------------------------------------------------------------------------------------------
+
+require('shelljs/global');
+
+// set directories and files for test and coverage report
+var path = require('path'),
+
+  NYC = require('nyc'),
+  chalk = require('chalk'),
+  recursive = require('recursive-readdir'),
+
+  COV_REPORT_PATH = '.coverage',
+  SPEC_SOURCE_DIR = path.join(__dirname, '..', 'test', 'newman');
+
+module.exports = function (exit) {
+  // banner line
+  console.info(chalk.yellow.bold('Running newman tests using mocha on node...'));
+
+  test('-d', COV_REPORT_PATH) && rm('-rf', COV_REPORT_PATH);
+  mkdir('-p', COV_REPORT_PATH);
+
+  var Mocha = require('mocha'),
+    nyc = new NYC({
+      reportDir: COV_REPORT_PATH,
+      tempDirectory: COV_REPORT_PATH,
+      reporter: ['text', 'lcov', 'text-summary'],
+      exclude: ['config', 'test'],
+      hookRunInContext: true,
+      hookRunInThisContext: true
+    });
+
+  nyc.wrap();
+  // add all spec files to mocha
+  recursive(SPEC_SOURCE_DIR, function (err, files) {
+    if (err) { console.error(err); return exit(1); }
+
+    var mocha = new Mocha({ timeout: 1000 * 60 });
+
+    files.filter(function (file) { // extract all test files
+      return (file.substr(-8) === '.test.js');
+    }).forEach(mocha.addFile.bind(mocha));
+
+    mocha.run(function (runError) {
+      runError && console.error(runError.stack || runError);
+
+      nyc.reset();
+      nyc.writeCoverageFile();
+      nyc.report();
+      exit(runError ? 1 : 0);
+    });
+  });
+};
+
+// ensure we run this script exports if this is a direct stdin.tty run
+!module.parent && module.exports(exit);
diff --git a/codegens/dart-http/npm/test-unit.js b/codegens/dart-http/npm/test-unit.js
new file mode 100644
index 0000000..1bd787a
--- /dev/null
+++ b/codegens/dart-http/npm/test-unit.js
@@ -0,0 +1,59 @@
+#!/usr/bin/env node
+/* eslint-env node, es6 */
+// ---------------------------------------------------------------------------------------------------------------------
+// This script is intended to execute all unit tests.
+// ---------------------------------------------------------------------------------------------------------------------
+
+require('shelljs/global');
+
+// set directories and files for test and coverage report
+var path = require('path'),
+
+  NYC = require('nyc'),
+  chalk = require('chalk'),
+  recursive = require('recursive-readdir'),
+
+  COV_REPORT_PATH = '.coverage',
+  SPEC_SOURCE_DIR = path.join(__dirname, '..', 'test', 'unit');
+
+module.exports = function (exit) {
+  // banner line
+  console.info(chalk.yellow.bold('Running unit tests using mocha on node...'));
+
+  test('-d', COV_REPORT_PATH) && rm('-rf', COV_REPORT_PATH);
+  mkdir('-p', COV_REPORT_PATH);
+
+  var Mocha = require('mocha'),
+    nyc = new NYC({
+      reportDir: COV_REPORT_PATH,
+      tempDirectory: COV_REPORT_PATH,
+      reporter: ['text', 'lcov', 'text-summary'],
+      exclude: ['config', 'test'],
+      hookRunInContext: true,
+      hookRunInThisContext: true
+    });
+
+  nyc.wrap();
+  // add all spec files to mocha
+  recursive(SPEC_SOURCE_DIR, function (err, files) {
+    if (err) { console.error(err); return exit(1); }
+
+    var mocha = new Mocha({ timeout: 1000 * 60 });
+
+    files.filter(function (file) { // extract all test files
+      return (file.substr(-8) === '.test.js');
+    }).forEach(mocha.addFile.bind(mocha));
+
+    mocha.run(function (runError) {
+      runError && console.error(runError.stack || runError);
+
+      nyc.reset();
+      nyc.writeCoverageFile();
+      nyc.report();
+      exit(runError ? 1 : 0);
+    });
+  });
+};
+
+// ensure we run this script exports if this is a direct stdin.tty run
+!module.parent && module.exports(exit);
diff --git a/codegens/dart-http/npm/test.js b/codegens/dart-http/npm/test.js
new file mode 100644
index 0000000..cff8a6c
--- /dev/null
+++ b/codegens/dart-http/npm/test.js
@@ -0,0 +1,20 @@
+#!/usr/bin/env node
+var chalk = require('chalk'),
+  exit = require('shelljs').exit,
+  prettyms = require('pretty-ms'),
+  startedAt = Date.now(),
+  name = require('../package.json').name;
+
+require('async').series([
+  require('./test-lint'),
+  require('./test-newman'),
+  require('./test-unit')
+  // Add a separate folder for every new suite of tests
+  // require('./test-unit')
+  // require('./test-browser')
+  // require('./test-integration')
+], function (code) {
+  // eslint-disable-next-line max-len
+  console.info(chalk[code ? 'red' : 'green'](`\n${name}: duration ${prettyms(Date.now() - startedAt)}\n${name}: ${code ? 'not ok' : 'ok'}!`));
+  exit(code && (typeof code === 'number' ? code : 1) || 0);
+});
diff --git a/codegens/dart-http/test/newman/newman.test.js b/codegens/dart-http/test/newman/newman.test.js
new file mode 100644
index 0000000..2b4e3e1
--- /dev/null
+++ b/codegens/dart-http/test/newman/newman.test.js
@@ -0,0 +1,16 @@
+var runNewmanTest = require('../../../../test/codegen/newman/newmanTestUtil').runNewmanTest,
+  convert = require('../../lib/index').convert;
+
+describe('Convert for different types of request', function () {
+  var options = {indentCount: 2, indentType: 'Space', includeBoilerplate: true },
+    testConfig = {
+      runScript: 'dart snippet.dart',
+      fileName: 'snippet.dart',
+      headerSnippet: '',
+      // http uses Map<String, String> to store headers, so there is no way to
+      // keep multiple headers with the same key
+      skipCollections: ['sameNameHeadersCollection']
+    };
+
+  runNewmanTest(convert, options, testConfig);
+});
diff --git a/codegens/dart-http/test/unit/convert.test.js b/codegens/dart-http/test/unit/convert.test.js
new file mode 100644
index 0000000..05e943c
--- /dev/null
+++ b/codegens/dart-http/test/unit/convert.test.js
@@ -0,0 +1,150 @@
+var convert = require('../../index').convert,
+  expect = require('chai').expect,
+  sdk = require('postman-collection');
+
+// Disable check with expected snippets as we now have proper newman tests
+describe('Dart Converter', function () {
+  it('should add timeout if requestTimeout options is used', function () {
+    var request = new sdk.Request({
+      'method': 'POST',
+      'header': [
+        {
+          'key': 'Content-Type',
+          'value': 'application/json'
+        }
+      ],
+      'body': {
+        'mode': 'raw',
+        'raw': '{\n  "json": "Test-Test"\n}'
+      },
+      'url': {
+        'raw': 'https://postman-echo.com/post',
+        'protocol': 'https',
+        'host': [
+          'postman-echo',
+          'com'
+        ],
+        'path': [
+          'post'
+        ]
+      }
+    });
+
+    convert(request, {requestTimeout: 5000}, function (err, snippet) {
+      if (err) {
+        expect.fail(err);
+      }
+      expect(snippet).to.be.a('string');
+      expect(snippet).to.contain('.timeout(Duration(milliseconds: 5000))');
+    });
+  });
+
+  it('should use http.MultipartRequest for formdata requests', function () {
+    var request = new sdk.Request({
+      'method': 'POST',
+      'header': [],
+      'body': {
+        'mode': 'formdata',
+        'formdata': []
+      },
+      'url': {
+        'raw': 'https://postman-echo.com/post',
+        'protocol': 'https',
+        'host': [
+          'postman-echo',
+          'com'
+        ],
+        'path': [
+          'post'
+        ]
+      }
+    });
+    convert(request, {}, function (err, snippet) {
+      if (err) {
+        expect.fail(err);
+      }
+      expect(snippet).to.be.a('string');
+      expect(snippet).to.contain('http.MultipartRequest');
+    });
+  });
+
+  it('should add code for followRedirects if given in the option', function () {
+    var request = new sdk.Request({
+      'method': 'GET',
+      'header': [],
+      'url': {
+        'raw': 'https://postman-echo.com/',
+        'protocol': 'https',
+        'host': [
+          'postman-echo',
+          'com'
+        ]
+      }
+    });
+    convert(request, { followRedirect: false }, function (err, snippet) {
+      if (err) {
+        expect.fail(err);
+      }
+      expect(snippet).to.be.a('string');
+      expect(snippet).to.contain('request.followRedirects = false;');
+    });
+  });
+
+  it('should add boilerplate if given in the option', function () {
+    var request = new sdk.Request({
+      'method': 'GET',
+      'header': [],
+      'url': {
+        'raw': 'https://postman-echo.com/',
+        'protocol': 'https',
+        'host': [
+          'postman-echo',
+          'com'
+        ]
+      }
+    });
+    convert(request, { includeBoilerplate: true }, function (err, snippet) {
+      if (err) {
+        expect.fail(err);
+      }
+      expect(snippet).to.be.a('string');
+      expect(snippet).to.contain('import \'package:http/http.dart\' as http;');
+      expect(snippet).to.contain('void main() async {');
+    });
+  });
+
+  it('should add correct indentation', function () {
+    var request = new sdk.Request({
+      'method': 'POST',
+      'header': [],
+      'body': {
+        'mode': 'formdata',
+        'formdata': [
+          {
+            'key': 'hello',
+            'value': 'world',
+            'type': 'text'
+          }
+        ]
+      },
+      'url': {
+        'raw': 'https://postman-echo.com/post',
+        'protocol': 'https',
+        'host': [
+          'postman-echo',
+          'com'
+        ],
+        'path': [
+          'post'
+        ]
+      }
+    });
+    convert(request, { includeBoilerplate: true, indentType: 'Tab' }, function (err, snippet) {
+      if (err) {
+        expect.fail(err);
+      }
+      expect(snippet).to.be.a('string');
+      expect(snippet).to.contain('\t\t\'hello\': \'world\'');
+    });
+  });
+});