<commit_before>chai.should();

describe('schema-form.provider.js', function() {
  beforeEach(module('schemaForm'));
  describe('#defaults()', function() {
    it('should generate default form def from a schema', function() {
      inject(function(schemaForm) {
        var schema = {
          "type": "object",
          "properties": {
            "name": {
              "title": "Name",
              "description": "Gimme yea name lad",
              "type": "string"
            },
            "gender": {
              "title": "Choose",
              "type": "string",
              "enum": [
                "undefined",
                "null",
                "NaN",
              ]
            },
            "overEighteen": {
              "title": "Are you over 18 years old?",
              "type": "boolean",
              "default": false
            },
            "attributes": {
              "type": "object",
              "required": ['eyecolor'],
              "properties": {
                "eyecolor": { "type": "string", "title": "Eye color" },
                "haircolor": { "type": "string", "title": "Hair color" },
                "shoulders": {
                  "type": "object",
                  "title": "Shoulders",
                  "properties": {
                    "left": { "type": "string" },
                    "right": { "type": "string" },
                  }
                }
              }
            }
          }
        };

        var form = [
          {
            "title": "Name",
            "description": "Gimme yea name lad",
            "schema": {
              "title": "Name",
              "description": "Gimme yea name lad",
              "type": "string"
            },
            "ngModelOptions": {},
            "key": [
              "name"
            ],
            "type": "text"
          },
          {
            "title": "Choose",
            "schema": {
              "title": "Choose",
              "type": "string",
              "enum": [
                "undefined",
                "null",
                "NaN"
              ]
            },
            "ngModelOptions": {},
            "key": [
              "gender"
            ],
            "type": "select",
            "titleMap": [
              {
                "name": "undefined",
                "value": "undefined"
              },
              {
                "name": "null",
                "value": "null"
              },
              {
                "name": "NaN",
                "value": "NaN"
              }
            ]
          },
          {
            "title": "Are you over 18 years old?",
            "schema": {
              "title": "Are you over 18 years old?",
              "type": "boolean",
              "default": false
            },
            "ngModelOptions": {},
            "key": [
              "overEighteen"
            ],
            "type": "checkbox"
          },
          {
            "title": "attributes",
            "schema": {
              "type": "object",
              "required": [
                "eyecolor"
              ],
              "properties": {
                "eyecolor": {
                  "type": "string",
                  "title": "Eye color"
                },
                "haircolor": {
                  "type": "string",
                  "title": "Hair color"
                },
                "shoulders": {
                  "type": "object",
                  "title": "Shoulders",
                  "properties": {
                    "left": {
                      "type": "string"
                    },
                    "right": {
                      "type": "string"
                    }
                  }
                }
              }
            },
            "ngModelOptions": {},
            "key": [
              "attributes"
            ],
            "type": "fieldset",
            "key": [
              "attributes"
            ],
            "items": [
              {
                "title": "Eye color",
                "required": true,
                "schema": {
                  "type": "string",
                  "title": "Eye color"
                },
                "ngModelOptions": {},
                "key": [
                  "attributes",
                  "eyecolor"
                ],
                "type": "text"
              },
              {
                "title": "Hair color",
                "schema": {
                  "type": "string",
                  "title": "Hair color"
                },
                "ngModelOptions": {},
                "key": [
                  "attributes",
                  "haircolor"
                ],
                "type": "text"
              },
              {
                "title": "Shoulders",
                "schema": {
                  "type": "object",
                  "title": "Shoulders",
                  "properties": {
                    "left": {
                      "type": "string"
                    },
                    "right": {
                      "type": "string"
                    }
                  }
                },
                "ngModelOptions": {},
                "key": [
                  "attributes",
                  "shoulders"
                ],
                "type": "fieldset",
                "key": [
                  "attributes",
                  "shoulders"
                ],
                "items": [
                  {
                    "title": "left",
                    "schema": {
                      "type": "string"
                    },
                    "ngModelOptions": {},
                    "key": [
                      "attributes",
                      "shoulders",
                      "left"
                    ],
                    "type": "text"
                  },
                  {
                    "title": "right",
                    "schema": {
                      "type": "string"
                    },
                    "ngModelOptions": {},
                    "key": [
                      "attributes",
                      "shoulders",
                      "right"
                    ],
                    "type": "text"
                  }
                ]
              }
            ]
          }
        ];

        var f = schemaForm.defaults(schema);
        f.form.should.be.deep.equal(form);
      });
    });

    it('should handle global defaults',function(){
      inject(function(schemaForm){

        var schema = {
          "type": "object",
          "properties": {
            "name": {
              "title": "Name",
              "description": "Gimme yea name lad",
              "type": "string"
            }
          }
        };

        var form = [
          {
            "title": "Name",
            "description": "Gimme yea name lad",
            "schema": {
              "title": "Name",
              "description": "Gimme yea name lad",
              "type": "string"
            },
            "ngModelOptions": { "updateOn": "blur"},
            "foo": "bar",
            "key": [
              "name"
            ],
            "type": "text"
          }
        ];

        var f = schemaForm.defaults(schema, schemaForm.typeDefault, {}, { formDefaults: { foo: "bar", ngModelOptions: { updateOn: 'blur' }}});
        f.form.should.be.deep.equal(form);
      });
    });

    it('should handle x-schema-form defaults',function(){
      inject(function(schemaForm){

        var schema = {
          "type": "object",
          "properties": {
            "name": {
              "title": "Name",
              "description": "Gimme yea name lad",
              "type": "string",
              "x-schema-form": {
                "type": "textarea"
              }
            }
          }
        };

        var f = schemaForm.defaults(schema, schemaForm.typeDefault, {});
        f.form[0].type.should.be.eq('textarea');
      });
    });

    it('should ignore parts of schema in ignore list',function(){
      inject(function(schemaForm){

        var schema = {
          "type": "object",
          "properties": {
            "name": {
              "title": "Name",
              "description": "Gimme yea name lad",
              "type": "string"
            },
            "gender": {
              "title": "Choose",
              "type": "string",
              "enum": [
                "undefined",
                "null",
                "NaN",
              ]
            }
          }
        };

        //no form is implicitly ['*']
        var defaults = schemaForm.defaults(schema).form;
        schemaForm.merge(schema,["*"], undefined, {gender:true}).should.be.deep.equal([defaults[0]]);
      });
    });
  });

  describe('#appendRule() and #prependRule()', function() {
    beforeEach(module('schemaForm'));
    beforeEach(
      module(function($sceProvider) {
        $sceProvider.enabled(false);
      })
    );

    it('should extend with new defaults',function(){
      module(function(schemaFormProvider){
        schemaFormProvider.prependRule('string', function(name, schema, options) {
          if (schema.format === 'foobar') {
            var f = schemaFormProvider.createStandardForm(name, schema, options);
            f.type = 'foobar';
            return f;
          }
        });

        schemaFormProvider.appendRule('string', function(name, schema, options){
          var f = schemaFormProvider.createStandardForm(name, schema, options);
          f.type = 'notused';
          return f;
        });
      });

      inject(function(schemaForm){

        var schema = {
          "type": "object",
          "properties": {
            "name": {
              "title": "Name",
              "format": "foobar",
              "description": "Gimme yea name lad",
              "type": "string"
            },
            "gender": {
              "title": "Choose",
              "type": "string",
              "enum": [
                "undefined",
                "null",
                "NaN",
              ]
            }
          }
        };

        //no form is implicitly ['*']
        var defaults = schemaForm.defaults(schema).form;
        defaults[0].type.should.be.equal('foobar');
        defaults[0].title.should.be.equal('Name');
        defaults[1].type.should.be.equal('select');
        defaults[1].title.should.be.equal('Choose');

      });
    });
  });

  describe('#postProcess()', function() {
    it('should be enable post-processing of forms',function(){
      module(function(schemaFormProvider){
        schemaFormProvider.postProcess(function(form){
          form.postProcess = true;
          form.length.should.be.eq(1);
          form[0].title.should.be.eq('Name');
          return form;
        });

      });

      inject(function(schemaForm){

        var schema = {
          "type": "object",
          "properties": {
            "name": {
              "title": "Name",
              "format": "foobar",
              "description": "Gimme yea name lad",
              "type": "string"
            }
          }
        };

        var form = schemaForm.merge(schema,["name"]);
        form.postProcess.should.be.true;

      });
    });
  });

  describe('#merge()', function() {
    it('should merge schema and form def',function(){
      inject(function(schemaForm){
        var schema = {
          "type": "object",
          "properties": {
            "name": {
              "title": "Name",
              "description": "Gimme yea name lad",
              "type": "string"
            },
            "gender": {
              "title": "Choose",
              "type": "string",
              "enum": [
                "undefined",
                "null",
                "NaN",
              ]
            }
          }
        };

        //no form is implicitly ['*']
        var defaults = schemaForm.defaults(schema).form;
        schemaForm.merge(schema).should.be.deep.equal(defaults);
        schemaForm.merge(schema,['*']).should.be.deep.equal(defaults);
        schemaForm.merge(schema,['*',{type:'fieldset'}]).should.be.deep.equal(defaults.concat([{type:'fieldset'}]));

        //simple form
        schemaForm.merge(schema,['gender']).should.be.deep.equal([defaults[1]]);
        schemaForm.merge(schema,['gender','name']).should.be.deep.equal([defaults[1],defaults[0]]);

        //change it up
        var f = angular.copy(defaults[0]);
        f.title = 'Foobar';
        f.type  = 'password';
        schemaForm.merge(schema,[{ key: 'name',title: 'Foobar',type: 'password'}]).should.be.deep.equal([f]);

      });
    });

    it('should translate "readOnly" in schema to "readonly" on the merged form defintion',function(){
      inject(function(schemaForm){
        var schema = {
          "type": "object",
          "properties": {
            "name": {
              "title": "Name",
              "description": "Gimme yea name lad",
              "type": "string"
            },
            "gender": {
              "readOnly": true,
              "title": "Choose",
              "type": "string",
              "enum": [
                "undefined",
                "null",
                "NaN",
              ]
            }
          }
        };

        var merged = schemaForm.merge(schema, ['gender']);
        merged[0].should.have.property('readonly');
        merged[0].readonly.should.eq(true)
      });
    });

    it('should push readOnly in schema down into objects and arrays', function() {
      inject(function(schemaForm) {
        var schema = {
          'type': 'object',
          'readOnly': true,
          'properties': {
            'sub': {
              'type': 'object',
              'properties': {
                'array': {
                  'type': 'array',
                  'items': {
                    'type': 'object',
                    'properties': {
                      'foo': {
                        'type': 'string'
                      }
                    }
                  }
                }
              }
            }
          }
        };

        var merged = schemaForm.merge(schema, ['*']);

        //sub
        merged[0].should.have.property('readonly');
        merged[0].readonly.should.eq(true);

        //array
        merged[0].items[0].should.have.property('readonly');
        merged[0].items[0].readonly.should.eq(true);

        //array items
        merged[0].items[0].items[0].should.have.property('readonly');
        merged[0].items[0].items[0].readonly.should.eq(true);

      });
    });

    it('should push readonly in form def down into objects and arrays', function() {
      inject(function(schemaForm) {
        var schema = {
          'type': 'object',
          'properties': {
            'sub': {
              'type': 'object',
              'properties': {
                'array': {
                  'type': 'array',
                  'items': {
                    'type': 'object',
                    'properties': {
                      'foo': {
                        'type': 'string'
                      }
                    }
                  }
                }
              }
            }
          }
        };

        var merged = schemaForm.merge(schema, [{key: 'sub', readonly: true}]);

        //sub
        merged[0].should.have.property('readonly');
        merged[0].readonly.should.eq(true);

        //array
        merged[0].items[0].should.have.property('readonly');
        merged[0].items[0].readonly.should.eq(true);

        //array items
        merged[0].items[0].items[0].should.have.property('readonly');
        merged[0].items[0].items[0].readonly.should.eq(true);

      });
    });

    it('regression test for merging schema that defines an array of objects #900', function() {
      inject(function(schemaForm) {

        var arrayObjectSchema = {
          type: "object",
          properties: {
            peopleLivingWithYou: {
              type: "object",
              properties: {
                dependentChildren: {
                  type: "array",
                  minItems: 1,
                  items: {
                    type: "object",
                    properties: {
                      name: {
                        title: "Name",
                        type: "string"
                      },
                      age: {
                        title: "Age",
                        type: "integer"
                      }
                    },
                    required: ["name"]
                  }
                }
              },
              required: ["dependentChildren"]
            }
          },
          required: ["peopleLivingWithYou"]
        };

        var formInsideSection = [{
          type: 'section',
          items: [{
            key: 'peopleLivingWithYou.dependentChildren',
            add: "Add Child",
            title: 'Dependent children details',
            validationMessage: 'Complete all required fields for at least one child'
          }]
        }];

        var merged = schemaForm.merge(arrayObjectSchema, formInsideSection);
        var objectPropertyKeys = merged[0].items[0].items[0];
        var nameKey = objectPropertyKeys.items[0].key;
        var ageKey = objectPropertyKeys.items[1].key;

        nameKey.join('.').should.eq("peopleLivingWithYou.dependentChildren..name");
        ageKey.join('.').should.eq("peopleLivingWithYou.dependentChildren..age");
      });
    });
  });
});
<commit_msg>add regression test for #900 that tests a form without a section<commit_after>chai.should();

describe('schema-form.provider.js', function() {
  beforeEach(module('schemaForm'));
  describe('#defaults()', function() {
    it('should generate default form def from a schema', function() {
      inject(function(schemaForm) {
        var schema = {
          "type": "object",
          "properties": {
            "name": {
              "title": "Name",
              "description": "Gimme yea name lad",
              "type": "string"
            },
            "gender": {
              "title": "Choose",
              "type": "string",
              "enum": [
                "undefined",
                "null",
                "NaN",
              ]
            },
            "overEighteen": {
              "title": "Are you over 18 years old?",
              "type": "boolean",
              "default": false
            },
            "attributes": {
              "type": "object",
              "required": ['eyecolor'],
              "properties": {
                "eyecolor": { "type": "string", "title": "Eye color" },
                "haircolor": { "type": "string", "title": "Hair color" },
                "shoulders": {
                  "type": "object",
                  "title": "Shoulders",
                  "properties": {
                    "left": { "type": "string" },
                    "right": { "type": "string" },
                  }
                }
              }
            }
          }
        };

        var form = [
          {
            "title": "Name",
            "description": "Gimme yea name lad",
            "schema": {
              "title": "Name",
              "description": "Gimme yea name lad",
              "type": "string"
            },
            "ngModelOptions": {},
            "key": [
              "name"
            ],
            "type": "text"
          },
          {
            "title": "Choose",
            "schema": {
              "title": "Choose",
              "type": "string",
              "enum": [
                "undefined",
                "null",
                "NaN"
              ]
            },
            "ngModelOptions": {},
            "key": [
              "gender"
            ],
            "type": "select",
            "titleMap": [
              {
                "name": "undefined",
                "value": "undefined"
              },
              {
                "name": "null",
                "value": "null"
              },
              {
                "name": "NaN",
                "value": "NaN"
              }
            ]
          },
          {
            "title": "Are you over 18 years old?",
            "schema": {
              "title": "Are you over 18 years old?",
              "type": "boolean",
              "default": false
            },
            "ngModelOptions": {},
            "key": [
              "overEighteen"
            ],
            "type": "checkbox"
          },
          {
            "title": "attributes",
            "schema": {
              "type": "object",
              "required": [
                "eyecolor"
              ],
              "properties": {
                "eyecolor": {
                  "type": "string",
                  "title": "Eye color"
                },
                "haircolor": {
                  "type": "string",
                  "title": "Hair color"
                },
                "shoulders": {
                  "type": "object",
                  "title": "Shoulders",
                  "properties": {
                    "left": {
                      "type": "string"
                    },
                    "right": {
                      "type": "string"
                    }
                  }
                }
              }
            },
            "ngModelOptions": {},
            "key": [
              "attributes"
            ],
            "type": "fieldset",
            "key": [
              "attributes"
            ],
            "items": [
              {
                "title": "Eye color",
                "required": true,
                "schema": {
                  "type": "string",
                  "title": "Eye color"
                },
                "ngModelOptions": {},
                "key": [
                  "attributes",
                  "eyecolor"
                ],
                "type": "text"
              },
              {
                "title": "Hair color",
                "schema": {
                  "type": "string",
                  "title": "Hair color"
                },
                "ngModelOptions": {},
                "key": [
                  "attributes",
                  "haircolor"
                ],
                "type": "text"
              },
              {
                "title": "Shoulders",
                "schema": {
                  "type": "object",
                  "title": "Shoulders",
                  "properties": {
                    "left": {
                      "type": "string"
                    },
                    "right": {
                      "type": "string"
                    }
                  }
                },
                "ngModelOptions": {},
                "key": [
                  "attributes",
                  "shoulders"
                ],
                "type": "fieldset",
                "key": [
                  "attributes",
                  "shoulders"
                ],
                "items": [
                  {
                    "title": "left",
                    "schema": {
                      "type": "string"
                    },
                    "ngModelOptions": {},
                    "key": [
                      "attributes",
                      "shoulders",
                      "left"
                    ],
                    "type": "text"
                  },
                  {
                    "title": "right",
                    "schema": {
                      "type": "string"
                    },
                    "ngModelOptions": {},
                    "key": [
                      "attributes",
                      "shoulders",
                      "right"
                    ],
                    "type": "text"
                  }
                ]
              }
            ]
          }
        ];

        var f = schemaForm.defaults(schema);
        f.form.should.be.deep.equal(form);
      });
    });

    it('should handle global defaults',function(){
      inject(function(schemaForm){

        var schema = {
          "type": "object",
          "properties": {
            "name": {
              "title": "Name",
              "description": "Gimme yea name lad",
              "type": "string"
            }
          }
        };

        var form = [
          {
            "title": "Name",
            "description": "Gimme yea name lad",
            "schema": {
              "title": "Name",
              "description": "Gimme yea name lad",
              "type": "string"
            },
            "ngModelOptions": { "updateOn": "blur"},
            "foo": "bar",
            "key": [
              "name"
            ],
            "type": "text"
          }
        ];

        var f = schemaForm.defaults(schema, schemaForm.typeDefault, {}, { formDefaults: { foo: "bar", ngModelOptions: { updateOn: 'blur' }}});
        f.form.should.be.deep.equal(form);
      });
    });

    it('should handle x-schema-form defaults',function(){
      inject(function(schemaForm){

        var schema = {
          "type": "object",
          "properties": {
            "name": {
              "title": "Name",
              "description": "Gimme yea name lad",
              "type": "string",
              "x-schema-form": {
                "type": "textarea"
              }
            }
          }
        };

        var f = schemaForm.defaults(schema, schemaForm.typeDefault, {});
        f.form[0].type.should.be.eq('textarea');
      });
    });

    it('should ignore parts of schema in ignore list',function(){
      inject(function(schemaForm){

        var schema = {
          "type": "object",
          "properties": {
            "name": {
              "title": "Name",
              "description": "Gimme yea name lad",
              "type": "string"
            },
            "gender": {
              "title": "Choose",
              "type": "string",
              "enum": [
                "undefined",
                "null",
                "NaN",
              ]
            }
          }
        };

        //no form is implicitly ['*']
        var defaults = schemaForm.defaults(schema).form;
        schemaForm.merge(schema,["*"], undefined, {gender:true}).should.be.deep.equal([defaults[0]]);
      });
    });
  });

  describe('#appendRule() and #prependRule()', function() {
    beforeEach(module('schemaForm'));
    beforeEach(
      module(function($sceProvider) {
        $sceProvider.enabled(false);
      })
    );

    it('should extend with new defaults',function(){
      module(function(schemaFormProvider){
        schemaFormProvider.prependRule('string', function(name, schema, options) {
          if (schema.format === 'foobar') {
            var f = schemaFormProvider.createStandardForm(name, schema, options);
            f.type = 'foobar';
            return f;
          }
        });

        schemaFormProvider.appendRule('string', function(name, schema, options){
          var f = schemaFormProvider.createStandardForm(name, schema, options);
          f.type = 'notused';
          return f;
        });
      });

      inject(function(schemaForm){

        var schema = {
          "type": "object",
          "properties": {
            "name": {
              "title": "Name",
              "format": "foobar",
              "description": "Gimme yea name lad",
              "type": "string"
            },
            "gender": {
              "title": "Choose",
              "type": "string",
              "enum": [
                "undefined",
                "null",
                "NaN",
              ]
            }
          }
        };

        //no form is implicitly ['*']
        var defaults = schemaForm.defaults(schema).form;
        defaults[0].type.should.be.equal('foobar');
        defaults[0].title.should.be.equal('Name');
        defaults[1].type.should.be.equal('select');
        defaults[1].title.should.be.equal('Choose');

      });
    });
  });

  describe('#postProcess()', function() {
    it('should be enable post-processing of forms',function(){
      module(function(schemaFormProvider){
        schemaFormProvider.postProcess(function(form){
          form.postProcess = true;
          form.length.should.be.eq(1);
          form[0].title.should.be.eq('Name');
          return form;
        });

      });

      inject(function(schemaForm){

        var schema = {
          "type": "object",
          "properties": {
            "name": {
              "title": "Name",
              "format": "foobar",
              "description": "Gimme yea name lad",
              "type": "string"
            }
          }
        };

        var form = schemaForm.merge(schema,["name"]);
        form.postProcess.should.be.true;

      });
    });
  });

  describe('#merge()', function() {
    it('should merge schema and form def',function(){
      inject(function(schemaForm){
        var schema = {
          "type": "object",
          "properties": {
            "name": {
              "title": "Name",
              "description": "Gimme yea name lad",
              "type": "string"
            },
            "gender": {
              "title": "Choose",
              "type": "string",
              "enum": [
                "undefined",
                "null",
                "NaN",
              ]
            }
          }
        };

        //no form is implicitly ['*']
        var defaults = schemaForm.defaults(schema).form;
        schemaForm.merge(schema).should.be.deep.equal(defaults);
        schemaForm.merge(schema,['*']).should.be.deep.equal(defaults);
        schemaForm.merge(schema,['*',{type:'fieldset'}]).should.be.deep.equal(defaults.concat([{type:'fieldset'}]));

        //simple form
        schemaForm.merge(schema,['gender']).should.be.deep.equal([defaults[1]]);
        schemaForm.merge(schema,['gender','name']).should.be.deep.equal([defaults[1],defaults[0]]);

        //change it up
        var f = angular.copy(defaults[0]);
        f.title = 'Foobar';
        f.type  = 'password';
        schemaForm.merge(schema,[{ key: 'name',title: 'Foobar',type: 'password'}]).should.be.deep.equal([f]);

      });
    });

    it('should translate "readOnly" in schema to "readonly" on the merged form defintion',function(){
      inject(function(schemaForm){
        var schema = {
          "type": "object",
          "properties": {
            "name": {
              "title": "Name",
              "description": "Gimme yea name lad",
              "type": "string"
            },
            "gender": {
              "readOnly": true,
              "title": "Choose",
              "type": "string",
              "enum": [
                "undefined",
                "null",
                "NaN",
              ]
            }
          }
        };

        var merged = schemaForm.merge(schema, ['gender']);
        merged[0].should.have.property('readonly');
        merged[0].readonly.should.eq(true)
      });
    });

    it('should push readOnly in schema down into objects and arrays', function() {
      inject(function(schemaForm) {
        var schema = {
          'type': 'object',
          'readOnly': true,
          'properties': {
            'sub': {
              'type': 'object',
              'properties': {
                'array': {
                  'type': 'array',
                  'items': {
                    'type': 'object',
                    'properties': {
                      'foo': {
                        'type': 'string'
                      }
                    }
                  }
                }
              }
            }
          }
        };

        var merged = schemaForm.merge(schema, ['*']);

        //sub
        merged[0].should.have.property('readonly');
        merged[0].readonly.should.eq(true);

        //array
        merged[0].items[0].should.have.property('readonly');
        merged[0].items[0].readonly.should.eq(true);

        //array items
        merged[0].items[0].items[0].should.have.property('readonly');
        merged[0].items[0].items[0].readonly.should.eq(true);

      });
    });

    it('should push readonly in form def down into objects and arrays', function() {
      inject(function(schemaForm) {
        var schema = {
          'type': 'object',
          'properties': {
            'sub': {
              'type': 'object',
              'properties': {
                'array': {
                  'type': 'array',
                  'items': {
                    'type': 'object',
                    'properties': {
                      'foo': {
                        'type': 'string'
                      }
                    }
                  }
                }
              }
            }
          }
        };

        var merged = schemaForm.merge(schema, [{key: 'sub', readonly: true}]);

        //sub
        merged[0].should.have.property('readonly');
        merged[0].readonly.should.eq(true);

        //array
        merged[0].items[0].should.have.property('readonly');
        merged[0].items[0].readonly.should.eq(true);

        //array items
        merged[0].items[0].items[0].should.have.property('readonly');
        merged[0].items[0].items[0].readonly.should.eq(true);

      });
    });

    var arrayObjectSchema = {
      type: "object",
      properties: {
        peopleLivingWithYou: {
          type: "object",
          properties: {
            dependentChildren: {
              type: "array",
              minItems: 1,
              items: {
                type: "object",
                properties: {
                  name: {
                    title: "Name",
                    type: "string"
                  },
                  age: {
                    title: "Age",
                    type: "integer"
                  }
                },
                required: ["name"]
              }
            }
          },
          required: ["dependentChildren"]
        }
      },
      required: ["peopleLivingWithYou"]
    };

    it('merge a schema that defines an array of objects with a form inside a section #900', function() {
      inject(function(schemaForm) {

        var formInsideSection = [{
          type: 'section',
          items: [{
            key: 'peopleLivingWithYou.dependentChildren',
            add: "Add Child",
            title: 'Dependent children details',
            validationMessage: 'Complete all required fields for at least one child'
          }]
        }];

        var merged = schemaForm.merge(arrayObjectSchema, formInsideSection);
        var objectPropertyKeys = merged[0].items[0].items[0];
        var nameKey = objectPropertyKeys.items[0].key;
        var ageKey = objectPropertyKeys.items[1].key;

        nameKey.join('.').should.eq("peopleLivingWithYou.dependentChildren..name");
        ageKey.join('.').should.eq("peopleLivingWithYou.dependentChildren..age");
      });
    });

    it('merge a schema that defines an array of objects with a form without a section #900', function() {
      inject(function(schemaForm) {

        var formWithoutSection = [{
          key: 'peopleLivingWithYou.dependentChildren',
          add: "Add Child",
          title: 'Dependent children details',
          validationMessage: 'Complete all required fields for at least one child'
        }];

        var merged = schemaForm.merge(arrayObjectSchema, formWithoutSection);
        var objectPropertyKeys = merged[0].items[0];
        var nameKey = objectPropertyKeys.items[0].key;
        var ageKey = objectPropertyKeys.items[1].key;

        nameKey.join('.').should.eq("peopleLivingWithYou.dependentChildren..name");
        ageKey.join('.').should.eq("peopleLivingWithYou.dependentChildren..age");
      });
    });
  });
});
<|endoftext|>


<commit_before>var editableList = (function () {
"use strict";

function editableListItem(list, model, items) {
    var item = el("div");
    var content;

    item.className = "listItem";

    item.onkeypress = function (evt) {
	switch (evt.keyCode) {
	case 13:
	    item.onEnterKey();
	    evt.preventDefault();
	    break;
	};
    };

    item.onfocus = function (evt) {
	list.setSelected(item);
	content = item.getContent();
	setTimeout(item.selectAll, 1);
    };

    item.onblur = function (evt) {
	if (item.innerHTML === content) {
	    return;
	}
	item.setEditable(false);
	model.update(items.indexOf(item), {content: item.getContent()});
    };

    item.onEnterKey = function (content) {
	item.blur();
    };

    item.getContent = function (content) {
	return item.innerHTML;
    };

    item.selectAll = function () {
	document.getSelection().selectAllChildren(item);
    };

    item.setContent = function (content) {
	item.innerHTML = content;
    };

    item.setEditable = function (editable) {
	item.contentEditable = editable;
    };

    return item;
}

function editableList(model, ret, listItem) {
    var items = [];
    var selected;
    var editing = "false";

    function itemAdded(index, attrs) {
	var item = listItem(ret, model, items);
	var attr;

	console.log("item added " + attrs.content);

	ret.insertBefore(item, items[index]);
	items.splice(index, 0, item);

	for (attr in attrs) {
	    model.itemChanged(index, attr, attrs[attr]);
	}

	item.setEditable(editing);
	item.focus();
    }

    function itemChanged(index, attr, value) {
	var item = items[index];

	if (!item) {
	    return;
	}

	if (attr == "content") {
	    item.setContent(value);

	    if (editing == "true") {
		item.setEditable(true);
	    }
	} else {
	    item.setAttribute(attr, value);
	}
    }

    function itemRemoved(index) {
	var item = items[index];
	var unused = { focus: function () {}};
	var hilight;

	items.splice(index, 1);

	if (item === selected) {
	    hilight = (item.nextSibling
			|| item.previousSibling
			|| unused);
	    hilight.focus && hilight.focus();
	}

	item && ret.removeChild(item);
    }

    ret.append = function () {
	model.insert(items.indexOf(selected) + 1, {});
    };

    ret.remove = function () {
	if (items.length) {
	    model.remove(items.indexOf(selected));
	}
    };

    ret.getEditMode = function (mode) {
	return editing;
    };

    ret.setEditMode = function (mode) {
	var li;
	editing = mode;
	for (li = ret.firstChild; li !== null; li = li.nextSibling) {
	    li.setEditable(mode);
	}
    };

    ret.setModel = function (newModel) {
	model = newModel;

	if (model) {
	    model.itemAdded = itemAdded;
	    model.itemChanged = itemChanged;
	    model.itemRemoved = itemRemoved;
	}

	ret.innerHTML = "";
    };

    ret.setSelected = function (item) {
	selected = item;
    };

    ret.setModel(model);

    return ret;
}

function listModel(defaultItem) {
    var items = [];
    var ret = {};

    ret.itemChanged = function () {};
    ret.itemAdded = function () {};
    ret.itemRemoved = function () {};

    ret.forEach = function (closure) {
	items.forEach(closure);
    };

    ret.getItem = function (i) {
	return items[i];
    };

    ret.getLength = function () {
	return items.length;
    };

    ret.update = function (index, attrs) {
	var attr;
	var value;

	for (attr in attrs) {
	    value = attrs[attr];
	    items[index][attr] = value;
	    ret.itemChanged(index, attr, value);
	}
    };

    ret.insert = function(index, attrs) {
	console.log("insert");
	attrs.content = attrs.content || defaultItem;
	items.splice(index, 0, attrs);
	ret.itemAdded(index, attrs);
    };

    ret.remove = function (index) {
	items.splice(index, 1);
	ret.itemRemoved (index);
    };

    return ret;
}

return {
    list: editableList,
    model: listModel,
    item: editableListItem
};

})();<commit_msg>expand documentation comments<commit_after>/* editableList.js
 *
 * An MVC and ajax-friendly list controler with user-editable items.
 *
 * editableList.list - Transforms an element into an editable list.
 *
 *  Once "listified", the element will ensure that there is exactly
 *  one child element for each item in its model.
 *
 *  Any html element that can contain children can become an editable
 *  list by calling editableList.list() on it directly.
 *
 *  An alternative way to create editable lists is with the
 *  <editable-list> tag. This module registers a page onload handler
 *  which will automagically call editableList.list() on the
 *  element. You may wish to include editableList.css to apply a basic
 *  default style.
 *
 * editableList.item - A factory function to create editable list
 *  items.
 *
 *  The default implementation presents a plain list item with no
 *  additional controls. It can be customized if different behavior is
 *  desired.
 *
 *  When the "edit mode" of the parent list is truthy, list items are
 *  made directly editable. The default behavior is to update the list
 *  model when an item loses focus. The current implementation also
 *  requires that the browser support the contentEditable attribute
 *  correctly.
 *
 *  List Item Interface:
 *
 *  + onEnterKey(): called when the enter key is pressed on the
 *   element. The default behavior is to blur the item, thereby
 *   commiting changes to the model.
 *
 *  + getContent(): returns the current content of the item.
 *
 *  + setContent(): updates the current content of the item.
 *
 *  + selectAll():  selects the contents of the item
 *
 *  + setEditable(editable): sets the edit state of the item. should be
 *    boolean true or false
 *
 *
 * Requires: util.js
 */

var editableList = (function () {
"use strict";

function editableListItem(list, model, items) {
    var item = el("div");
    var content;

    item.className = "listItem";

    item.onkeypress = function (evt) {
	switch (evt.keyCode) {
	case 13:
	    item.onEnterKey();
	    evt.preventDefault();
	    break;
	};
    };

    item.onfocus = function (evt) {
	list.setSelected(item);
	content = item.getContent();
	setTimeout(item.selectAll, 1);
    };

    item.onblur = function (evt) {
	if (item.innerHTML === content) {
	    return;
	}
	item.setEditable(false);
	model.update(items.indexOf(item), {content: item.getContent()});
    };

    item.onEnterKey = function (content) {
	item.blur();
    };

    item.getContent = function (content) {
	return item.innerHTML;
    };

    item.selectAll = function () {
	document.getSelection().selectAllChildren(item);
    };

    item.setContent = function (content) {
	item.innerHTML = content;
    };

    item.setEditable = function (editable) {
	item.contentEditable = editable;
    };

    return item;
}

function editableList(model, ret, listItem) {
    var items = [];
    var selected;
    var editing = "false";

    function itemAdded(index, attrs) {
	var item = listItem(ret, model, items);
	var attr;

	console.log("item added " + attrs.content);

	ret.insertBefore(item, items[index]);
	items.splice(index, 0, item);

	for (attr in attrs) {
	    model.itemChanged(index, attr, attrs[attr]);
	}

	item.setEditable(editing);
	item.focus();
    }

    function itemChanged(index, attr, value) {
	var item = items[index];

	if (!item) {
	    return;
	}

	if (attr == "content") {
	    item.setContent(value);

	    if (editing == "true") {
		item.setEditable(true);
	    }
	} else {
	    item.setAttribute(attr, value);
	}
    }

    function itemRemoved(index) {
	var item = items[index];
	var unused = { focus: function () {}};
	var hilight;

	items.splice(index, 1);

	if (item === selected) {
	    hilight = (item.nextSibling
			|| item.previousSibling
			|| unused);
	    hilight.focus && hilight.focus();
	}

	item && ret.removeChild(item);
    }

    ret.append = function () {
	model.insert(items.indexOf(selected) + 1, {});
    };

    ret.remove = function () {
	if (items.length) {
	    model.remove(items.indexOf(selected));
	}
    };

    ret.getEditMode = function (mode) {
	return editing;
    };

    ret.setEditMode = function (mode) {
	var li;
	editing = mode;
	for (li = ret.firstChild; li !== null; li = li.nextSibling) {
	    li.setEditable(mode);
	}
    };

    ret.setModel = function (newModel) {
	model = newModel;

	if (model) {
	    model.itemAdded = itemAdded;
	    model.itemChanged = itemChanged;
	    model.itemRemoved = itemRemoved;
	}

	ret.innerHTML = "";
    };

    ret.setSelected = function (item) {
	selected = item;
    };

    ret.setModel(model);

    return ret;
}

function listModel(defaultItem) {
    var items = [];
    var ret = {};

    ret.itemChanged = function () {};
    ret.itemAdded = function () {};
    ret.itemRemoved = function () {};

    ret.forEach = function (closure) {
	items.forEach(closure);
    };

    ret.getItem = function (i) {
	return items[i];
    };

    ret.getLength = function () {
	return items.length;
    };

    ret.update = function (index, attrs) {
	var attr;
	var value;

	for (attr in attrs) {
	    value = attrs[attr];
	    items[index][attr] = value;
	    ret.itemChanged(index, attr, value);
	}
    };

    ret.insert = function(index, attrs) {
	console.log("insert");
	attrs.content = attrs.content || defaultItem;
	items.splice(index, 0, attrs);
	ret.itemAdded(index, attrs);
    };

    ret.remove = function (index) {
	items.splice(index, 1);
	ret.itemRemoved (index);
    };

    return ret;
}

return {
    list: editableList,
    model: listModel,
    item: editableListItem
};

})();<|endoftext|>


<commit_before>
Ui.CanvasElement.extend('Ui.CheckBoxGraphic', {
	isDown: false,
	isChecked: false,
	radius: 4,
	color: undefined,
	checkColor: undefined,

	constructor: function(config) {
		this.color = new Ui.Color({ r: 1, g: 1, b: 1 });
		this.checkColor = new Ui.Color({ r: 0.31, g: 0.66, b: 0.31 });
	},

	getIsDown: function() {
		return this.isDown;
	},

	setIsDown: function(isDown) {
		if(this.isDown != isDown) {
			this.isDown = isDown;
			this.invalidateDraw();
		}
	},

	getIsChecked: function() {
		return this.isChecked;
	},

	setIsChecked: function(isChecked) {
		if(this.isChecked != isChecked) {
			this.isChecked = isChecked;
			this.invalidateDraw();
		}
	},

	setRadius: function(radius) {
		if(this.radius != radius) {
			this.radius = radius;
			this.invalidateMeasure();
			this.invalidateDraw();
		}
	},

	getColor: function() {
		return this.color;
	},

	setColor: function(color) {
		if(this.color != color) {
			this.color = Ui.Color.create(color);
			this.invalidateDraw();
		}
	},

	setCheckColor: function(color) {
		if(this.checkColor != color) {
			this.checkColor = Ui.Color.create(color);
			this.invalidateDraw();
		}
	},

	getCheckColor: function() {
		var deltaY = 0;
		if(this.getIsDown())
			deltaY = 0.20;
		var yuv = this.checkColor.getYuv();
		return new Ui.Color({ y: yuv.y + deltaY, u: yuv.u, v: yuv.v });
	}
}, {
	updateCanvas: function(ctx) {
		var w = this.getLayoutWidth();
		var h = this.getLayoutHeight();
		var cx = w/2;
		var cy = h/2;

		// background
		if(this.getIsDown())
			ctx.globalAlpha = 0.8;
		ctx.fillStyle = this.getColor().getCssRgba();
		ctx.beginPath();
		ctx.fillRect(cx-10, cy-10, 20, 20);
		ctx.closePath();
		ctx.fill();

		// handle disable
		if(this.getIsDisabled())
			ctx.globalAlpha = 0.2;

		// border
		ctx.strokeStyle = 'rgb(100,100,100)';
		ctx.lineWidth = 2;
		ctx.strokeRect(cx-10, cy-10, 20, 20);

		if(this.isChecked) {
			// icon
			var iconSize = 20;
			var path = Ui.Icon.getPath('check');
			var scale = iconSize/48;
			// icon
			ctx.save();
			ctx.translate((w-iconSize)/2, (h-iconSize)/2);
			ctx.scale(scale, scale);
			ctx.fillStyle = this.getCheckColor().getCssRgba();
			ctx.beginPath();
			this.svgPath(path);
			ctx.closePath();
			ctx.fill();
			ctx.restore();
		}
	},

	measureCore: function(width, height) {
		return { width: 30, height: 30 };
	},

	onDisable: function() {
		this.invalidateDraw();
	},

	onEnable: function() {
		this.invalidateDraw();
	}
});
<commit_msg>remove radius, remove the background<commit_after>
Ui.CanvasElement.extend('Ui.CheckBoxGraphic', {
	isDown: false,
	isChecked: false,
	color: undefined,
	checkColor: undefined,

	constructor: function(config) {
		this.color = new Ui.Color({ r: 1, g: 1, b: 1 });
		this.checkColor = new Ui.Color({ r: 0.31, g: 0.66, b: 0.31 });
	},

	getIsDown: function() {
		return this.isDown;
	},

	setIsDown: function(isDown) {
		if(this.isDown != isDown) {
			this.isDown = isDown;
			this.invalidateDraw();
		}
	},

	getIsChecked: function() {
		return this.isChecked;
	},

	setIsChecked: function(isChecked) {
		if(this.isChecked != isChecked) {
			this.isChecked = isChecked;
			this.invalidateDraw();
		}
	},

	getColor: function() {
		return this.color;
	},

	setColor: function(color) {
		if(this.color != color) {
			this.color = Ui.Color.create(color);
			this.invalidateDraw();
		}
	},

	setCheckColor: function(color) {
		if(this.checkColor != color) {
			this.checkColor = Ui.Color.create(color);
			this.invalidateDraw();
		}
	},

	getCheckColor: function() {
		var deltaY = 0;
		if(this.getIsDown())
			deltaY = 0.20;
		var yuv = this.checkColor.getYuv();
		return new Ui.Color({ y: yuv.y + deltaY, u: yuv.u, v: yuv.v });
	}
}, {
	updateCanvas: function(ctx) {
		var w = this.getLayoutWidth();
		var h = this.getLayoutHeight();
		var cx = w/2;
		var cy = h/2;

		// background
		if(this.getIsDown())
			ctx.globalAlpha = 0.8;

		// handle disable
		if(this.getIsDisabled())
			ctx.globalAlpha = 0.2;

		// border
		ctx.strokeStyle = this.getColor().getCssRgba();
		ctx.lineWidth = 2;
		ctx.strokeRect(cx-10, cy-10, 20, 20);

		if(this.isChecked) {
			// icon
			var iconSize = 20;
			var path = Ui.Icon.getPath('check');
			var scale = iconSize/48;
			// icon
			ctx.save();
			ctx.translate((w-iconSize)/2, (h-iconSize)/2);
			ctx.scale(scale, scale);
			ctx.fillStyle = this.getCheckColor().getCssRgba();
			ctx.beginPath();
			ctx.svgPath(path);
			ctx.closePath();
			ctx.fill();
			ctx.restore();
		}
	},

	measureCore: function(width, height) {
		return { width: 30, height: 30 };
	},

	onDisable: function() {
		this.invalidateDraw();
	},

	onEnable: function() {
		this.invalidateDraw();
	}
});
<|endoftext|>


<commit_before>function grid_init() {
    $('.good_guy_choice').click(function(e){
        $('.scorebox.win1, .scorebox.loss1').toggleClass('win1 loss1');
        $('.scorebox.win2, .scorebox.loss2').toggleClass('win2 loss2');
        $('.scorebox.win3, .scorebox.loss3').toggleClass('win3 loss3');
        $('.scorebox.win4, .scorebox.loss4').toggleClass('win4 loss4');
        $('.good_guy_choice').toggleClass('good_guy bad_guy');
    });
}

function choose_delta_class(game) {
    if (ben.is_good_guy) {
        delta = ben_team.score - brian_team.score;
    } else {
        delta = brian_team.score - ben_team.score;
    }

    /* Choose win/loss/tie. */
    var ret = 'tied';
    if (delta < 0) {
        ret = 'loss';
    } else if (delta > 0) {
        ret = 'win';
    }

    /* Set level of win/loss. */
    delta = abs(delta);
    if (game.is_final()) {
        ret += '4';
    } else if (delta > 14) {
        ret += '4';
    } else if (delta > 7) {
        ret += '3';
    } else if (delta > 3) {
        ret += '2';
    } else if (delta > 0) {
        ret += '1';
    }
    return ret;
}

function choose_team(game, teams) {
    return (game.away.name in teams) ? game.away : game.home;
}

function build_team_row(team) {
    return $('<tr>')
        .append($('<td>').attr('class', 'team_name').text(team.name))
        .append($('<td>').attr('class', 'game_score').text(team.score));
}

function build_table(game) {
    away_row = build_team_row(game.away);
    home_row = build_team_row(game.home);
    time_row = $('<tr>')
        .append($('<td>'))
        .append($('<td>').attr('class', 'time'));
    $('<table>').attr('class', 'score')
        .append(away_row)
        .append(time_row)
        .append(home_row);
}

function update_game(game) {
    var box = $('#' + game.id);

    box.innerHTML(build_table(game));
    ben_team = choose_team(game, ben.teams);
    brian_team = choose_team(game, brian.teams);
    box.attr('class', choose_delta_class(game));
}

function update_wins() {
    $("#ben_wins > .total").text(ben.total_wins);
    $("#ben_wins > .wins").text(ben.final_wins);
    $("#brian_wins > .total").text(brian.total_wins);
    $("#brian_wins > .wins").text(brian.final_wins);
}

function update_grid(games) {
    ben.reset_wins();
    brian.reset_wins();
    for (var game in games) {
        update_game(game);
        ben.update(game);
        brian.update(game);
    }
    update_win_totals();
}<commit_msg>Allow good guy to be set via the location hash.<commit_after>function grid_init() {
    $('.good_guy_choice').click(function(e){
        $('.scorebox.win1, .scorebox.loss1').toggleClass('win1 loss1');
        $('.scorebox.win2, .scorebox.loss2').toggleClass('win2 loss2');
        $('.scorebox.win3, .scorebox.loss3').toggleClass('win3 loss3');
        $('.scorebox.win4, .scorebox.loss4').toggleClass('win4 loss4');
        $('.good_guy_choice').toggleClass('good_guy bad_guy');
    });
    /* Ben is the default good guy, unless told otherwise. */
    if (location.hash == '#brian') {
        $('.bad_guy').click();
    }
}

function choose_delta_class(game) {
    if (ben.is_good_guy) {
        delta = ben_team.score - brian_team.score;
    } else {
        delta = brian_team.score - ben_team.score;
    }

    /* Choose win/loss/tie. */
    var ret = 'tied';
    if (delta < 0) {
        ret = 'loss';
    } else if (delta > 0) {
        ret = 'win';
    }

    /* Set level of win/loss. */
    delta = abs(delta);
    if (game.is_final()) {
        ret += '4';
    } else if (delta > 14) {
        ret += '4';
    } else if (delta > 7) {
        ret += '3';
    } else if (delta > 3) {
        ret += '2';
    } else if (delta > 0) {
        ret += '1';
    }
    return ret;
}

function choose_team(game, teams) {
    return (game.away.name in teams) ? game.away : game.home;
}

function build_team_row(team) {
    return $('<tr>')
        .append($('<td>').attr('class', 'team_name').text(team.name))
        .append($('<td>').attr('class', 'game_score').text(team.score));
}

function build_table(game) {
    away_row = build_team_row(game.away);
    home_row = build_team_row(game.home);
    time_row = $('<tr>')
        .append($('<td>'))
        .append($('<td>').attr('class', 'time'));
    $('<table>').attr('class', 'score')
        .append(away_row)
        .append(time_row)
        .append(home_row);
}

function update_game(game) {
    var box = $('#' + game.id);

    box.innerHTML(build_table(game));
    ben_team = choose_team(game, ben.teams);
    brian_team = choose_team(game, brian.teams);
    box.attr('class', choose_delta_class(game));
}

function update_wins() {
    $("#ben_wins > .total").text(ben.total_wins);
    $("#ben_wins > .wins").text(ben.final_wins);
    $("#brian_wins > .total").text(brian.total_wins);
    $("#brian_wins > .wins").text(brian.final_wins);
}

function update_grid(games) {
    ben.reset_wins();
    brian.reset_wins();
    for (var game in games) {
        update_game(game);
        ben.update(game);
        brian.update(game);
    }
    update_win_totals();
}<|endoftext|>


<commit_before>const { parse } = require('papaparse');
const stripBOM = require('strip-bom');
const { mix } = require('mixwith');

const Syncer = require('./Syncer');
const ClassImporter = require('./mixins/ClassImporter');

const ATTRIBUTES = [
	{ name: 'namePrefix', aliases: ['nameprefix', 'prefix', 'title', 'affix'] },
	{ name: 'firstName', required: true, aliases: ['firstname', 'first', 'first-name', 'fn'] },
	{ name: 'middleName', aliases: ['middlename', 'middle'] },
	{ name: 'lastName', required: true, aliases: ['lastname', 'last', 'last-name', 'n'] },
	{ name: 'nameSuffix', aliases: ['namesuffix', 'suffix'] },
	{ name: 'email', required: true, aliases: ['email', 'mail', 'e-mail'] },
	{ name: 'class', aliases: ['class', 'classes'] },
];

/**
 * Returns a function that transforms objects of the source schema to the target schema
 * @param {Object} sourceSchema Object representing the source schema
 * @param {Array<Object>} targetSchema Target schema as array of attribute properties
 * `[{name: String, Aliases: Array<String>}, ...]`
 * @example
 * const mf = buildMappingFunction({foo: 'bar'}, {name: 'test', aliases: ['foo', 'baz']});
 * mf({foo: 'bar'}) === {test: 'bar'} // true
 * mf({foo: 'hello'}) === {test: 'hello'} // true
 * mf({baz: 42}) === {} // baz is an alias of test, but is not in the source schema
 */
const buildMappingFunction = (sourceSchema, targetSchema = ATTRIBUTES) => {
	const mapping = {};
	Object.keys(sourceSchema).forEach((key) => {
		const attribute = targetSchema.find((a) => a.aliases.includes(key.toLowerCase()));
		if (attribute !== undefined) {
			mapping[key] = attribute.name;
		}
	});
	return (record) => Object.keys(mapping).reduce((res, key) => {
		res[mapping[key]] = record[key];
		return res;
	}, {});
};

/**
 * Implements importing CSV documents based on the Syncer interface
 * @class CSVSyncer
 * @implements {Syncer}
 */
class CSVSyncer extends mix(Syncer).with(ClassImporter) {
	constructor(app, stats = {}, logger, options = {}, requestParams = {}) {
		super(app, stats, logger);

		this.options = options;
		this.requestParams = requestParams;

		Object.assign(this.stats, {
			users: {
				successful: 0,
				created: 0,
				updated: 0,
				failed: 0,
			},
			invitations: {
				successful: 0,
				failed: 0,
			},
		});
	}

	/**
     * @see {Syncer#respondsTo}
     */
	static respondsTo(target) {
		return target === 'csv';
	}

	/**
     * @see {Syncer#params}
     */
	static params(params, data = {}) {
		const query = (params || {}).query || {};
		if (query.school && data.data) {
			return [
				{
					// required
					schoolId: query.school,
					csvData: data.data,
					// optional
					role: query.role || 'student',
					sendEmails: query.sendEmails === 'true',
					schoolYear: query.schoolYear,
				},
				params,
			];
		}
		return false;
	}

	/**
     * @see {Syncer#steps}
     */
	async steps() {
		await super.steps();
		this.options.schoolYear = await this.determineSchoolYear();
		const records = this.parseCsvData();
		const sanitizedRecords = CSVSyncer.sanitizeRecords(records);
		const importClasses = CSVSyncer.needsToImportClasses(sanitizedRecords);
		const clusteredRecords = this.clusterByEmail(sanitizedRecords);

		const actions = Object.values(clusteredRecords).map((record) => async () => {
			const enrichedRecord = await this.enrichUserData(record);
			const user = await this.createOrUpdateUser(enrichedRecord);
			if (importClasses) {
				await this.createClasses(enrichedRecord, user);
			}
		});

		while (actions.length > 0) {
			const action = actions.shift();
			await action.apply(this);
		}

		return this.stats;
	}

	async determineSchoolYear() {
		try {
			if (this.options.schoolYear) {
				return this.app.service('years').get(this.options.schoolYear);
			}
			const school = await this.app.service('schools').get(this.options.schoolId);
			return this.app.service('years').get(school.currentYear);
		} catch (err) {
			this.logError('Cannot determine school year to import from params', {
				paramSchoolYear: this.options.schoolYear,
				paramSchool: this.options.school,
			});
		}
		return undefined;
	}

	parseCsvData() {
		let records = [];
		try {
			const strippedData = stripBOM(this.options.csvData);
			const parseResult = parse(strippedData, {
				delimiter: '',	// auto-detect
				newline: '',	// auto-detect
				header: true,
				skipEmptyLines: true,
				fastMode: true,
			});
			const { errors } = parseResult;
			if (Array.isArray(errors) && errors.length > 0) {
				errors.forEach((error) => {
					this.logWarning('Skipping line, because it contains an error', { error });
					this.stats.errors.push({
						type: 'file',
						entity: 'Eingabedatei fehlerhaft',
						message: `Syntaxfehler in Zeile ${error.row}`,
					});
				});
			}
			records = parseResult.data;
		} catch (error) {
			this.stats.errors.push({
				type: 'file',
				entity: 'Eingabedatei fehlerhaft',
				message: 'Datei ist nicht im korrekten Format',
			});
			throw error;
		}

		if (!Array.isArray(records) || records.length === 0) {
			this.logError('Parsing failed: No input data.');
			this.stats.errors.push({
				type: 'file',
				entity: 'Eingabedatei fehlerhaft',
				message: 'Datei enthält keine Daten',
			});
			throw new Error('No input data');
		}

		ATTRIBUTES.filter((a) => a.required).forEach((attr) => {
			const attributeIsUsed = Object.keys(records[0]).some((k) => attr.aliases.includes(k.toLowerCase()));
			if (!attributeIsUsed) {
				this.stats.errors.push({
					type: 'file',
					entity: 'Eingabedatei fehlerhaft',
					message: `benötigtes Attribut "${attr.name}" nicht gefunden`,
				});
				throw new Error(`Parsing failed. Expected attribute "${attr.name}"`);
			}
		});
		return records;
	}

	static needsToImportClasses(records) {
		return records[0].class !== undefined;
	}

	static sanitizeRecords(records) {
		const mappingFunction = buildMappingFunction(records[0]);
		return records.map((record) => {
			const mappedRecord = mappingFunction(record);
			mappedRecord.email = mappedRecord.email.trim().toLowerCase();
			return mappedRecord;
		});
	}

	clusterByEmail(records) {
		const recordsByEmail = {};
		records.forEach(async (record) => {
			if (recordsByEmail[record.email]) {
				this.stats.errors.push({
					type: 'user',
					entity: `${record.firstName},${record.lastName},${record.email}`,
					message: `Mehrfachnutzung der E-Mail-Adresse "${record.email}". `
						+ 'Nur der erste Eintrag wurde importiert, dieser ignoriert.',
				});
				this.stats.users.failed += 1;
			} else {
				recordsByEmail[record.email] = record;
			}
		});
		return recordsByEmail;
	}

	async enrichUserData(record) {
		const linkData = await this.generateRegistrationLink({
			role: this.options.role,
			schoolId: this.options.schoolId,
			save: true,
			toHash: record.email,
		});
		const enrichedUser = Object.assign(record, {
			schoolId: this.options.schoolId,
			roles: [this.options.role],
			sendRegistration: true,
		});
		enrichedUser.importHash = linkData.hash;
		enrichedUser.shortLink = linkData.shortLink;
		return enrichedUser;
	}

	generateRegistrationLink(params) {
		return this.app.service('registrationlink').create(params);
	}

	async createOrUpdateUser(record) {
		const userId = await this.findUserIdForRecord(record);
		if (userId === null) {
			return this.createUser(record);
		}
		return this.updateUser(userId, record);
	}

	async findUserIdForRecord(record) {
		const users = await this.app.service('users').find({
			query: {
				email: record.email,
			},
			paginate: false,
			lean: true,
		}, this.requestParams);
		if (users.length >= 1) {
			return users[0]._id;
		}
		return null;
	}

	async createUser(record) {
		let userObject;
		try {
			userObject = await this.app.service('users').create(record, this.requestParams);
			this.stats.users.created += 1;
			this.stats.users.successful += 1;
			if (this.options.sendEmails) {
				await this.emailUser(record);
			}
		} catch (err) {
			this.logError('Cannot create user', record, JSON.stringify(err));
			this.handleUserError(err, record);
		}
		return userObject;
	}

	async updateUser(userId, record) {
		let userObject;
		try {
			const patch = {
				firstName: record.firstName,
				lastName: record.lastName,
			};
			const params = {
				/*
                query and payload need to be deleted, so that feathers doesn't want to update
                multiple database objects (or none in this case). We still need the rest of
                the requestParams to authenticate as Admin
                */
				...this.requestParams,
				query: undefined,
				payload: undefined,
			};
			userObject = await this.app.service('users').patch(userId, patch, params);
			this.stats.users.updated += 1;
			this.stats.users.successful += 1;
		} catch (err) {
			this.logError('Cannot update user', record, JSON.stringify(err));
			this.handleUserError(err, record);
		}
		return userObject;
	}

	handleUserError(err, record) {
		this.stats.users.failed += 1;
		if (err.message.startsWith('user validation failed')) {
			this.stats.errors.push({
				type: 'user',
				entity: `${record.firstName},${record.lastName},${record.email}`,
				message: `Ungültiger Wert in Spalte "${err.message.match(/Path `(.+)` is required/)[1]}"`,
			});
		} else {
			this.stats.errors.push({
				type: 'user',
				entity: `${record.firstName},${record.lastName},${record.email}`,
				message: err.message,
			});
		}
	}

	async emailUser(user) {
		try {
			if (user && user.email && user.schoolId && user.shortLink) {
				await this.app.service('mails').create({
					email: user.email,
					subject: `Einladung für die Nutzung der ${process.env.SC_TITLE}!`,
					headers: {},
					content: {
						text: `Einladung in die ${process.env.SC_TITLE}\n`
							+ `Hallo ${user.firstName} ${user.lastName}!\n\n`
							+ `Du wurdest eingeladen, der ${process.env.SC_TITLE} beizutreten, `
							+ 'bitte vervollständige deine Registrierung unter folgendem Link: '
							+ `${user.shortLink}\n\n`
							+ 'Viel Spaß und einen guten Start wünscht dir dein '
							+ `${process.env.SC_SHORT_TITLE}-Team`,
					},
				});
				this.stats.invitations.successful += 1;
			} else {
				throw new Error('Invalid user object');
			}
		} catch (err) {
			this.stats.invitations.failed += 1;
			this.stats.errors.push({
				type: 'invitation',
				entity: user.email,
				message: 'Die automatische Einladungs-E-Mail konnte nicht gesendet werden. Bitte manuell einladen.',
			});
			this.logError('Cannot send invitation link to user', err);
		}
	}

	async createClasses(record, user) {
		if (user === undefined) return;
		const classes = CSVSyncer.splitClasses(record.class);
		const classMapping = await this.buildClassMapping(classes, {
			schoolId: this.options.schoolId,
			year: this.options.schoolYear._id,
		});

		const actions = classes.map(async (klass) => {
			const classObject = classMapping[klass];
			if (classObject === undefined) return;

			const collection = this.options.role === 'teacher' ? 'teacherIds' : 'userIds';
			const importIds = classObject[collection].map((uid) => uid.toString());
			if (!importIds.includes(user._id.toString())) {
				const patchData = {};
				patchData[collection] = [...importIds, user._id.toString()];
				await this.app.service('/classes').patch(classObject._id, patchData);
			}
		});
		await Promise.all(actions);
	}

	static splitClasses(classes) {
		return classes.split('+').filter((name) => name !== '');
	}
}

module.exports = CSVSyncer;
<commit_msg>Make sure all required attributes exist for every record<commit_after>const { parse } = require('papaparse');
const stripBOM = require('strip-bom');
const { mix } = require('mixwith');

const Syncer = require('./Syncer');
const ClassImporter = require('./mixins/ClassImporter');

const ATTRIBUTES = [
	{ name: 'namePrefix', aliases: ['nameprefix', 'prefix', 'title', 'affix'] },
	{ name: 'firstName', required: true, aliases: ['firstname', 'first', 'first-name', 'fn'] },
	{ name: 'middleName', aliases: ['middlename', 'middle'] },
	{ name: 'lastName', required: true, aliases: ['lastname', 'last', 'last-name', 'n'] },
	{ name: 'nameSuffix', aliases: ['namesuffix', 'suffix'] },
	{ name: 'email', required: true, aliases: ['email', 'mail', 'e-mail'] },
	{ name: 'class', aliases: ['class', 'classes'] },
];

/**
 * Returns a function that transforms objects of the source schema to the target schema
 * @param {Object} sourceSchema Object representing the source schema
 * @param {Array<Object>} targetSchema Target schema as array of attribute properties
 * `[{name: String, Aliases: Array<String>}, ...]`
 * @example
 * const mf = buildMappingFunction({foo: 'bar'}, {name: 'test', aliases: ['foo', 'baz']});
 * mf({foo: 'bar'}) === {test: 'bar'} // true
 * mf({foo: 'hello'}) === {test: 'hello'} // true
 * mf({baz: 42}) === {} // baz is an alias of test, but is not in the source schema
 */
const buildMappingFunction = (sourceSchema, targetSchema = ATTRIBUTES) => {
	const mapping = {};
	Object.keys(sourceSchema).forEach((key) => {
		const attribute = targetSchema.find((a) => a.aliases.includes(key.toLowerCase()));
		if (attribute !== undefined) {
			mapping[key] = attribute.name;
		}
	});
	return (record) => Object.keys(mapping).reduce((res, key) => {
		res[mapping[key]] = record[key];
		return res;
	}, {});
};

/**
 * Implements importing CSV documents based on the Syncer interface
 * @class CSVSyncer
 * @implements {Syncer}
 */
class CSVSyncer extends mix(Syncer).with(ClassImporter) {
	constructor(app, stats = {}, logger, options = {}, requestParams = {}) {
		super(app, stats, logger);

		this.options = options;
		this.requestParams = requestParams;

		Object.assign(this.stats, {
			users: {
				successful: 0,
				created: 0,
				updated: 0,
				failed: 0,
			},
			invitations: {
				successful: 0,
				failed: 0,
			},
		});
	}

	/**
     * @see {Syncer#respondsTo}
     */
	static respondsTo(target) {
		return target === 'csv';
	}

	/**
     * @see {Syncer#params}
     */
	static params(params, data = {}) {
		const query = (params || {}).query || {};
		if (query.school && data.data) {
			return [
				{
					// required
					schoolId: query.school,
					csvData: data.data,
					// optional
					role: query.role || 'student',
					sendEmails: query.sendEmails === 'true',
					schoolYear: query.schoolYear,
				},
				params,
			];
		}
		return false;
	}

	/**
     * @see {Syncer#steps}
     */
	async steps() {
		await super.steps();
		this.options.schoolYear = await this.determineSchoolYear();
		const records = this.parseCsvData();
		const sanitizedRecords = CSVSyncer.sanitizeRecords(records);
		const importClasses = CSVSyncer.needsToImportClasses(sanitizedRecords);
		const clusteredRecords = this.clusterByEmail(sanitizedRecords);

		const actions = Object.values(clusteredRecords).map((record) => async () => {
			const enrichedRecord = await this.enrichUserData(record);
			const user = await this.createOrUpdateUser(enrichedRecord);
			if (importClasses) {
				await this.createClasses(enrichedRecord, user);
			}
		});

		while (actions.length > 0) {
			const action = actions.shift();
			await action.apply(this);
		}

		return this.stats;
	}

	async determineSchoolYear() {
		try {
			if (this.options.schoolYear) {
				return this.app.service('years').get(this.options.schoolYear);
			}
			const school = await this.app.service('schools').get(this.options.schoolId);
			return this.app.service('years').get(school.currentYear);
		} catch (err) {
			this.logError('Cannot determine school year to import from params', {
				paramSchoolYear: this.options.schoolYear,
				paramSchool: this.options.school,
			});
		}
		return undefined;
	}

	parseCsvData() {
		let records = [];
		try {
			const strippedData = stripBOM(this.options.csvData);
			const parseResult = parse(strippedData, {
				delimiter: '',	// auto-detect
				newline: '',	// auto-detect
				header: true,
				skipEmptyLines: true,
				fastMode: true,
			});
			const { errors } = parseResult;
			if (Array.isArray(errors) && errors.length > 0) {
				errors.forEach((error) => {
					this.logWarning('Skipping line, because it contains an error', { error });
					this.stats.errors.push({
						type: 'file',
						entity: 'Eingabedatei fehlerhaft',
						message: `Syntaxfehler in Zeile ${error.row}`,
					});
					this.stats.users.failed += 1;
				});
			}
			records = parseResult.data;
		} catch (error) {
			this.stats.errors.push({
				type: 'file',
				entity: 'Eingabedatei fehlerhaft',
				message: 'Datei ist nicht im korrekten Format',
			});
			throw error;
		}

		if (!Array.isArray(records) || records.length === 0) {
			this.logError('Parsing failed: No input data.');
			this.stats.errors.push({
				type: 'file',
				entity: 'Eingabedatei fehlerhaft',
				message: 'Datei enthält keine Daten',
			});
			throw new Error('No input data');
		}

		ATTRIBUTES.filter((a) => a.required).forEach((attr) => {
			const attributeIsUsed = Object.keys(records[0]).some((k) => attr.aliases.includes(k.toLowerCase()));
			if (!attributeIsUsed) {
				this.stats.errors.push({
					type: 'file',
					entity: 'Eingabedatei fehlerhaft',
					message: `benötigtes Attribut "${attr.name}" nicht gefunden`,
				});
				throw new Error(`Parsing failed. Expected attribute "${attr.name}"`);
			}
		});
		return records;
	}

	static needsToImportClasses(records) {
		return records[0].class !== undefined;
	}

	static sanitizeRecords(records) {
		const requiredAttributes = ATTRIBUTES.filter((a) => a.required).map((a) => a.name);
		const mappingFunction = buildMappingFunction(records[0]);
		const processed = [];
		records.forEach((record) => {
			const mappedRecord = mappingFunction(record);
			if (requiredAttributes.every((attr) => !!mappedRecord[attr])) {
				mappedRecord.email = mappedRecord.email.trim().toLowerCase();
				processed.push(mappedRecord);
			}
			// no else condition or errors necessary, because the error was reported
			// and logged during parsing
		});
		return processed;
	}

	clusterByEmail(records) {
		const recordsByEmail = {};
		records.forEach(async (record) => {
			if (recordsByEmail[record.email]) {
				this.stats.errors.push({
					type: 'user',
					entity: `${record.firstName},${record.lastName},${record.email}`,
					message: `Mehrfachnutzung der E-Mail-Adresse "${record.email}". `
						+ 'Nur der erste Eintrag wurde importiert, dieser ignoriert.',
				});
				this.stats.users.failed += 1;
			} else {
				recordsByEmail[record.email] = record;
			}
		});
		return recordsByEmail;
	}

	async enrichUserData(record) {
		const linkData = await this.generateRegistrationLink({
			role: this.options.role,
			schoolId: this.options.schoolId,
			save: true,
			toHash: record.email,
		});
		const enrichedUser = Object.assign(record, {
			schoolId: this.options.schoolId,
			roles: [this.options.role],
			sendRegistration: true,
		});
		enrichedUser.importHash = linkData.hash;
		enrichedUser.shortLink = linkData.shortLink;
		return enrichedUser;
	}

	generateRegistrationLink(params) {
		return this.app.service('registrationlink').create(params);
	}

	async createOrUpdateUser(record) {
		const userId = await this.findUserIdForRecord(record);
		if (userId === null) {
			return this.createUser(record);
		}
		return this.updateUser(userId, record);
	}

	async findUserIdForRecord(record) {
		const users = await this.app.service('users').find({
			query: {
				email: record.email,
			},
			paginate: false,
			lean: true,
		}, this.requestParams);
		if (users.length >= 1) {
			return users[0]._id;
		}
		return null;
	}

	async createUser(record) {
		let userObject;
		try {
			userObject = await this.app.service('users').create(record, this.requestParams);
			this.stats.users.created += 1;
			this.stats.users.successful += 1;
			if (this.options.sendEmails) {
				await this.emailUser(record);
			}
		} catch (err) {
			this.logError('Cannot create user', record, JSON.stringify(err));
			this.handleUserError(err, record);
		}
		return userObject;
	}

	async updateUser(userId, record) {
		let userObject;
		try {
			const patch = {
				firstName: record.firstName,
				lastName: record.lastName,
			};
			const params = {
				/*
                query and payload need to be deleted, so that feathers doesn't want to update
                multiple database objects (or none in this case). We still need the rest of
                the requestParams to authenticate as Admin
                */
				...this.requestParams,
				query: undefined,
				payload: undefined,
			};
			userObject = await this.app.service('users').patch(userId, patch, params);
			this.stats.users.updated += 1;
			this.stats.users.successful += 1;
		} catch (err) {
			this.logError('Cannot update user', record, JSON.stringify(err));
			this.handleUserError(err, record);
		}
		return userObject;
	}

	handleUserError(err, record) {
		this.stats.users.failed += 1;
		if (err.message.startsWith('user validation failed')) {
			this.stats.errors.push({
				type: 'user',
				entity: `${record.firstName},${record.lastName},${record.email}`,
				message: `Ungültiger Wert in Spalte "${err.message.match(/Path `(.+)` is required/)[1]}"`,
			});
		} else {
			this.stats.errors.push({
				type: 'user',
				entity: `${record.firstName},${record.lastName},${record.email}`,
				message: err.message,
			});
		}
	}

	async emailUser(user) {
		try {
			if (user && user.email && user.schoolId && user.shortLink) {
				await this.app.service('mails').create({
					email: user.email,
					subject: `Einladung für die Nutzung der ${process.env.SC_TITLE}!`,
					headers: {},
					content: {
						text: `Einladung in die ${process.env.SC_TITLE}\n`
							+ `Hallo ${user.firstName} ${user.lastName}!\n\n`
							+ `Du wurdest eingeladen, der ${process.env.SC_TITLE} beizutreten, `
							+ 'bitte vervollständige deine Registrierung unter folgendem Link: '
							+ `${user.shortLink}\n\n`
							+ 'Viel Spaß und einen guten Start wünscht dir dein '
							+ `${process.env.SC_SHORT_TITLE}-Team`,
					},
				});
				this.stats.invitations.successful += 1;
			} else {
				throw new Error('Invalid user object');
			}
		} catch (err) {
			this.stats.invitations.failed += 1;
			this.stats.errors.push({
				type: 'invitation',
				entity: user.email,
				message: 'Die automatische Einladungs-E-Mail konnte nicht gesendet werden. Bitte manuell einladen.',
			});
			this.logError('Cannot send invitation link to user', err);
		}
	}

	async createClasses(record, user) {
		if (user === undefined) return;
		const classes = CSVSyncer.splitClasses(record.class);
		const classMapping = await this.buildClassMapping(classes, {
			schoolId: this.options.schoolId,
			year: this.options.schoolYear._id,
		});

		const actions = classes.map(async (klass) => {
			const classObject = classMapping[klass];
			if (classObject === undefined) return;

			const collection = this.options.role === 'teacher' ? 'teacherIds' : 'userIds';
			const importIds = classObject[collection].map((uid) => uid.toString());
			if (!importIds.includes(user._id.toString())) {
				const patchData = {};
				patchData[collection] = [...importIds, user._id.toString()];
				await this.app.service('/classes').patch(classObject._id, patchData);
			}
		});
		await Promise.all(actions);
	}

	static splitClasses(classes) {
		return classes.split('+').filter((name) => name !== '');
	}
}

module.exports = CSVSyncer;
<|endoftext|>


<commit_before>var os = require('os');
var path = require('path');
var fs = require('fs');
var util = require('util');
var Promise = require('bluebird');
var whenReadFile = Promise.promisify(fs.readFile);

var minimatch = require('./lib/fnmatch');
var iniparser = require('./lib/ini');
var Version = require('./lib/version');
var pkg = require('./package.json');

var knownProps = [
  'end_of_line',
  'indent_style',
  'indent_size',
  'insert_final_newline',
  'trim_trailing_whitespace',
  'charset'
].reduce(function (set, prop) {
  set[prop] = true;
  return set;
}, {});

function fnmatch(filepath, glob) {
  var matchOptions = {matchBase: true, dot: true, noext: true};
  glob = glob.replace(/\*\*/g, '{*,**/**/**}');
  return minimatch(filepath, glob, matchOptions);
}

function getConfigFileNames(filepath, options) {
  var paths = [];
  do {
    filepath = path.dirname(filepath);
    paths.push(path.join(filepath, options.config));
  } while (filepath !== options.root);
  return paths;
}

function getFilepathRoot(filepath) {
  if (path.parse !== undefined) {
    // Node.js >= 0.11.15
    return path.parse(filepath).root;
  }
  if (os.platform() === 'win32') {
    return path.resolve(filepath).match(/^(\\\\[^\\]+\\)?[^\\]+\\/)[0];
  }
  return '/';
}

function processMatches(matches, version) {
  // Set indent_size to "tab" if indent_size is unspecified and
  // indent_style is set to "tab".
  if ("indent_style" in matches && matches.indent_style === "tab" &&
    !("indent_size" in matches) && version.gte(new Version(0, 10))) {
    matches.indent_size = "tab";
  }

  // Set tab_width to indent_size if indent_size is specified and
  // tab_width is unspecified
  if ("indent_size" in matches && !("tab_width" in matches) &&
  matches.indent_size !== "tab")
    matches.tab_width = matches.indent_size;

  // Set indent_size to tab_width if indent_size is "tab"
  if("indent_size" in matches && "tab_width" in matches &&
  matches.indent_size === "tab")
    matches.indent_size = matches.tab_width;

  return matches;
}

function processOptions(options, filepath) {
  options = options || {};
  return {
    config: options.config || '.editorconfig',
    version: new Version(options.version || pkg.version),
    root: path.resolve(options.root || getFilepathRoot(filepath))
  };
}

function buildFullGlob(pathPrefix, glob) {
  switch (glob.indexOf('/')) {
    case -1: glob = "**/" + glob; break;
    case  0: glob = glob.substring(1); break;
  }
  return path.join(pathPrefix, glob);
}

function extendProps(props, options) {
  for (var key in options) {
    var value = options[key];
    key = key.toLowerCase();
    if (knownProps[key]) {
      value = value.toLowerCase();
    }
    try {
      value = JSON.parse(value);
    } catch(e) {}
    if (typeof value === 'undefined' || value === null) {
      // null and undefined are values specific to JSON (no special meaning
      // in editorconfig) & should just be returned as regular strings.
      value = String(value);
    }
    props[key] = value;
  }
  return props;
}

function parseFromFiles(filepath, files, options) {
  return getConfigsForFiles(files).then(function (configs) {
    return configs.reverse();
  }).reduce(function (matches, file) {
    var pathPrefix = path.dirname(file.name);
    file.contents.forEach(function (section) {
      var glob = section[0], options = section[1];
      if (!glob) return;
      var fullGlob = buildFullGlob(pathPrefix, glob);
      if (!fnmatch(filepath, fullGlob)) return;
      matches = extendProps(matches, options);
    });
    return matches;
  }, {}).then(function (matches) {
    return processMatches(matches, options.version);
  });
}

function parseFromFilesSync(filepath, files, options) {
  var configs = getConfigsForFilesSync(files);
  configs.reverse();
  var matches = {};
  configs.forEach(function(config) {
    var pathPrefix = path.dirname(config.name);
    config.contents.forEach(function(section) {
      var glob = section[0], options = section[1];
      if (!glob) return;
      var fullGlob = buildFullGlob(pathPrefix, glob);
      if (!fnmatch(filepath, fullGlob)) return;
      matches = extendProps(matches, options);
    });
  });
  return processMatches(matches, options.version);
}

function StopReduce(array) {
  this.array = array;
}

StopReduce.prototype = Object.create(Error.prototype);

function getConfigsForFiles(files) {
  return Promise.reduce(files, function (configs, file) {
    var contents = iniparser.parseString(file.contents);
    configs.push({
      name: file.name,
      contents: contents
    });
    if ((contents[0][1].root || '').toLowerCase() === 'true') {
      return Promise.reject(new StopReduce(configs));
    }
    return configs;
  }, []).catch(StopReduce, function (stop) {
    return stop.array;
  });
}

function getConfigsForFilesSync(files) {
  var configs = [];
  for (var i in files) {
    var file = files[i];
    var contents = iniparser.parseString(file.contents);
    configs.push({
      name: file.name,
      contents: contents
    });
    if ((contents[0][1].root || '').toLowerCase() === 'true') {
      break;
    }
  };
  return configs;
}

function readConfigFiles(filepaths) {
  return Promise.map(filepaths, function (path) {
    return whenReadFile(path, 'utf-8').catch(function () {
      return '';
    }).then(function (contents) {
      return {name: path, contents: contents};
    });
  });
}

function readConfigFilesSync(filepaths) {
  var files = [];
  var file;
  filepaths.forEach(function(filepath) {
    try {
      file = fs.readFileSync(filepath, 'utf8');
    } catch (e) {
      file = '';
    }
    files.push({name: filepath, contents: file});
  });
  return files;
}

module.exports.parseFromFiles = function (filepath, files, options) {
  return new Promise (function (resolve, reject) {
    filepath = path.resolve(filepath);
    options = processOptions(options, filepath);
    resolve(parseFromFiles(filepath, files, options));
  });
};

module.exports.parseFromFilesSync = function (filepath, files, options) {
  filepath = path.resolve(filepath);
  options = processOptions(options, filepath);
  return parseFromFilesSync(filepath, files, options);
};

module.exports.parse = function (filepath, options) {
  return new Promise (function (resolve, reject) {
    filepath = path.resolve(filepath);
    options = processOptions(options, filepath);
    var filepaths = getConfigFileNames(filepath, options);
    var files = readConfigFiles(filepaths);
    resolve(parseFromFiles(filepath, files, options));
  });
};

module.exports.parseSync = function (filepath, options) {
    filepath = path.resolve(filepath);
    options = processOptions(options, filepath);
    var filepaths = getConfigFileNames(filepath, options);
    var files = readConfigFilesSync(filepaths);
    return parseFromFilesSync(filepath, files, options);
};
<commit_msg>sort requires<commit_after>var Promise = require('bluebird');
var fs = require('fs');
var os = require('os');
var path = require('path');
var util = require('util');
var whenReadFile = Promise.promisify(fs.readFile);

var Version = require('./lib/version');
var iniparser = require('./lib/ini');
var minimatch = require('./lib/fnmatch');
var pkg = require('./package.json');

var knownProps = [
  'end_of_line',
  'indent_style',
  'indent_size',
  'insert_final_newline',
  'trim_trailing_whitespace',
  'charset'
].reduce(function (set, prop) {
  set[prop] = true;
  return set;
}, {});

function fnmatch(filepath, glob) {
  var matchOptions = {matchBase: true, dot: true, noext: true};
  glob = glob.replace(/\*\*/g, '{*,**/**/**}');
  return minimatch(filepath, glob, matchOptions);
}

function getConfigFileNames(filepath, options) {
  var paths = [];
  do {
    filepath = path.dirname(filepath);
    paths.push(path.join(filepath, options.config));
  } while (filepath !== options.root);
  return paths;
}

function getFilepathRoot(filepath) {
  if (path.parse !== undefined) {
    // Node.js >= 0.11.15
    return path.parse(filepath).root;
  }
  if (os.platform() === 'win32') {
    return path.resolve(filepath).match(/^(\\\\[^\\]+\\)?[^\\]+\\/)[0];
  }
  return '/';
}

function processMatches(matches, version) {
  // Set indent_size to "tab" if indent_size is unspecified and
  // indent_style is set to "tab".
  if ("indent_style" in matches && matches.indent_style === "tab" &&
    !("indent_size" in matches) && version.gte(new Version(0, 10))) {
    matches.indent_size = "tab";
  }

  // Set tab_width to indent_size if indent_size is specified and
  // tab_width is unspecified
  if ("indent_size" in matches && !("tab_width" in matches) &&
  matches.indent_size !== "tab")
    matches.tab_width = matches.indent_size;

  // Set indent_size to tab_width if indent_size is "tab"
  if("indent_size" in matches && "tab_width" in matches &&
  matches.indent_size === "tab")
    matches.indent_size = matches.tab_width;

  return matches;
}

function processOptions(options, filepath) {
  options = options || {};
  return {
    config: options.config || '.editorconfig',
    version: new Version(options.version || pkg.version),
    root: path.resolve(options.root || getFilepathRoot(filepath))
  };
}

function buildFullGlob(pathPrefix, glob) {
  switch (glob.indexOf('/')) {
    case -1: glob = "**/" + glob; break;
    case  0: glob = glob.substring(1); break;
  }
  return path.join(pathPrefix, glob);
}

function extendProps(props, options) {
  for (var key in options) {
    var value = options[key];
    key = key.toLowerCase();
    if (knownProps[key]) {
      value = value.toLowerCase();
    }
    try {
      value = JSON.parse(value);
    } catch(e) {}
    if (typeof value === 'undefined' || value === null) {
      // null and undefined are values specific to JSON (no special meaning
      // in editorconfig) & should just be returned as regular strings.
      value = String(value);
    }
    props[key] = value;
  }
  return props;
}

function parseFromFiles(filepath, files, options) {
  return getConfigsForFiles(files).then(function (configs) {
    return configs.reverse();
  }).reduce(function (matches, file) {
    var pathPrefix = path.dirname(file.name);
    file.contents.forEach(function (section) {
      var glob = section[0], options = section[1];
      if (!glob) return;
      var fullGlob = buildFullGlob(pathPrefix, glob);
      if (!fnmatch(filepath, fullGlob)) return;
      matches = extendProps(matches, options);
    });
    return matches;
  }, {}).then(function (matches) {
    return processMatches(matches, options.version);
  });
}

function parseFromFilesSync(filepath, files, options) {
  var configs = getConfigsForFilesSync(files);
  configs.reverse();
  var matches = {};
  configs.forEach(function(config) {
    var pathPrefix = path.dirname(config.name);
    config.contents.forEach(function(section) {
      var glob = section[0], options = section[1];
      if (!glob) return;
      var fullGlob = buildFullGlob(pathPrefix, glob);
      if (!fnmatch(filepath, fullGlob)) return;
      matches = extendProps(matches, options);
    });
  });
  return processMatches(matches, options.version);
}

function StopReduce(array) {
  this.array = array;
}

StopReduce.prototype = Object.create(Error.prototype);

function getConfigsForFiles(files) {
  return Promise.reduce(files, function (configs, file) {
    var contents = iniparser.parseString(file.contents);
    configs.push({
      name: file.name,
      contents: contents
    });
    if ((contents[0][1].root || '').toLowerCase() === 'true') {
      return Promise.reject(new StopReduce(configs));
    }
    return configs;
  }, []).catch(StopReduce, function (stop) {
    return stop.array;
  });
}

function getConfigsForFilesSync(files) {
  var configs = [];
  for (var i in files) {
    var file = files[i];
    var contents = iniparser.parseString(file.contents);
    configs.push({
      name: file.name,
      contents: contents
    });
    if ((contents[0][1].root || '').toLowerCase() === 'true') {
      break;
    }
  };
  return configs;
}

function readConfigFiles(filepaths) {
  return Promise.map(filepaths, function (path) {
    return whenReadFile(path, 'utf-8').catch(function () {
      return '';
    }).then(function (contents) {
      return {name: path, contents: contents};
    });
  });
}

function readConfigFilesSync(filepaths) {
  var files = [];
  var file;
  filepaths.forEach(function(filepath) {
    try {
      file = fs.readFileSync(filepath, 'utf8');
    } catch (e) {
      file = '';
    }
    files.push({name: filepath, contents: file});
  });
  return files;
}

module.exports.parseFromFiles = function (filepath, files, options) {
  return new Promise (function (resolve, reject) {
    filepath = path.resolve(filepath);
    options = processOptions(options, filepath);
    resolve(parseFromFiles(filepath, files, options));
  });
};

module.exports.parseFromFilesSync = function (filepath, files, options) {
  filepath = path.resolve(filepath);
  options = processOptions(options, filepath);
  return parseFromFilesSync(filepath, files, options);
};

module.exports.parse = function (filepath, options) {
  return new Promise (function (resolve, reject) {
    filepath = path.resolve(filepath);
    options = processOptions(options, filepath);
    var filepaths = getConfigFileNames(filepath, options);
    var files = readConfigFiles(filepaths);
    resolve(parseFromFiles(filepath, files, options));
  });
};

module.exports.parseSync = function (filepath, options) {
    filepath = path.resolve(filepath);
    options = processOptions(options, filepath);
    var filepaths = getConfigFileNames(filepath, options);
    var files = readConfigFilesSync(filepaths);
    return parseFromFilesSync(filepath, files, options);
};
<|endoftext|>