text: #!/usr/bin/env python
  # This Source Code Form is subject to the terms of the Mozilla Public
  # License, v. 2.0. If a copy of the MPL was not distributed with this file,
  # You can obtain one at http://mozilla.org/MPL/2.0/.
  
  import json
  import pytoml
  import re
  import sys
  
  import six
  import voluptuous
  import voluptuous.humanize
  from voluptuous import Schema, Optional, Any, All, Required, Length, Range, Msg, Match
  
  
  Text = Any(six.text_type, six.binary_type)
  
  
  id_regex = re.compile(r'^[a-z0-9-]+$')
  feature_schema = Schema({
      Match(id_regex): {
          Required('title'): All(Text, Length(min=1)),
          Required('description'): All(Text, Length(min=1)),
          Required('bug-numbers'): All(Length(min=1), [All(int, Range(min=1))]),
          Required('restart-required'): bool,
          Required('type'): 'boolean', # In the future this may include other types
          Optional('preference'): Text,
          Optional('default-value'): Any(bool, dict), # the types of the keys here should match the value of `type`
          Optional('is-public'): Any(bool, dict),
          Optional('description-links'): dict,
      },
  })
  
  
  EXIT_OK = 0
  EXIT_ERROR = 1
  
  def main(output, *filenames):
      features = {}
      errors = False
      try:
          features = process_files(filenames)
          json.dump(features, output, sort_keys=True)
      except ExceptionGroup as error_group:
          print(str(error_group))
          return EXIT_ERROR
      return EXIT_OK
  
  
  class ExceptionGroup(Exception):
      def __init__(self, errors):
          self.errors = errors
  
      def __str__(self):
          rv = ['There were errors while processing feature definitions:']
          for error in self.errors:
              # indent the message
              s = '\n'.join('    ' + line for line in str(error).split('\n'))
              # add a * at the beginning of the first line
              s = '  * ' + s[4:]
              rv.append(s)
          return '\n'.join(rv)
  
  
  class FeatureGateException(Exception):
      def __init__(self, message, filename=None):
          super(FeatureGateException, self).__init__(message)
          self.filename = filename
  
      def __str__(self):
          message = super(FeatureGateException, self).__str__()
          rv = ["In"]
          if self.filename is None:
              rv.append("unknown file:")
          else:
              rv.append('file "{}":'.format(self.filename))
          rv.append(message)
          return ' '.join(rv)
  
      def __repr__(self):
          # Turn "FeatureGateExcept(<message>,)" into "FeatureGateException(<message>, filename=<filename>)"
          original = super(FeatureGateException, self).__repr__()
          return original[:-1] + ' filename={!r})'.format(self.filename)
  
  
  def process_files(filenames):
      features = {}
      errors = []
  
      for filename in filenames:
          try:
              with open(filename, 'r') as f:
                  feature_data = pytoml.load(f)
  
              voluptuous.humanize.validate_with_humanized_errors(feature_data, feature_schema)
  
              for feature_id, feature in feature_data.items():
                  feature['id'] = feature_id
                  features[feature_id] = expand_feature(feature)
          except (voluptuous.error.Error, IOError, FeatureGateException) as e:
              # Wrap errors in enough information to know which file they came from
              errors.append(FeatureGateException(e, filename))
          except pytoml.TomlError as e:
              # Toml errors have file information already
              errors.append(e)
  
      if errors:
          raise ExceptionGroup(errors)
  
      return features
  
  
  def hyphens_to_camel_case(s):
      """Convert names-with-hyphens to namesInCamelCase"""
      rv = ''
      for part in s.split('-'):
          if rv == '':
              rv = part.lower()
          else:
              rv += part[0].upper() + part[1:].lower()
      return rv
  
  
  
  def expand_feature(feature):
      """Fill in default values for optional fields"""
  
      # convert all names-with-hyphens to namesInCamelCase
      key_changes = []
      for key in feature.keys():
          if '-' in key:
              new_key = hyphens_to_camel_case(key)
              key_changes.append((key, new_key))
  
      for (old_key, new_key) in key_changes:
          feature[new_key] = feature[old_key]
          del feature[old_key]
  
      if feature['type'] == 'boolean':
          feature.setdefault('preference', 'features.{}.enabled'.format(feature['id']))
          feature.setdefault('defaultValue', False)
      elif 'preference' not in feature:
          raise FeatureGateException(
              'Features of type {} must specify an explicit preference name'.format(feature['type'])
          )
  
      feature.setdefault('isPublic', False)
  
      try:
          for key in ['defaultValue', 'isPublic']:
              feature[key] = process_configured_value(key, feature[key])
      except FeatureGateException as e:
          raise FeatureGateException(
              "Error when processing feature {}: {}".format(feature['id'], e.message))
  
      return feature
  
  
  def process_configured_value(name, value):
      if not isinstance(value, dict):
          return {'default': value}
  
      if 'default' not in value:
          raise FeatureGateException("Config for {} has no default: {}".format(name, value))
  
      expected_keys = set({'default', 'win', 'mac', 'linux', 'android', 'nightly', 'beta', 'release', 'dev-edition', 'esr'})
  
      for key in value.keys():
          parts = [p.strip() for p in key.split(",")]
          for part in parts:
              if part not in expected_keys:
                  raise FeatureGateException(
                      "Unexpected target {}, expected any of {}".format(part, expected_keys)
                  )
  
      # TODO Compute values at build time, so that it always returns only a single value.
  
      return value
  
  
  if __name__ == '__main__':
      sys.exit(main(sys.stdout, *sys.argv[1:]))
  