awm
awm

Reputation: 1200

How can I check if multiple properties exists on an object without being too verbose?

I am trying to verify that some properties exist on a configuration object and have values (truthy? not necessrily as a few pointed out in the comments) in javascript in the following manner:

const verifyJanrainAppSettings = (options) => {
    return options.JanrainAppSettings
      && options.JanrainAppSettings.settings.tokenUrl
      && options.JanrainAppSettings.settings.capture.clientId
      && options.JanrainAppSettings.settings.capture.appId
      && options.JanrainAppSettings.settings.capture.appDomain
      && options.JanrainAppSettings.settings.capture.captureServer
      && options.JanrainAppSettings.settings.httpLoadUrl
      && options.JanrainAppSettings.settings.httpsLoadUrl
}

I feel this is too verbose and I am curious if there is a better pattern. Perhaps something like if (['my', 'options'] in myObject) ...

Upvotes: 10

Views: 5318

Answers (5)

lhrinaldi
lhrinaldi

Reputation: 53

I would opt for an utility function:

function contains(object, ...keys) {
  return keys.every((key) => key in object)
}

const verifyJanrainAppSettings = (options) => {
  return (
    contains(options.JanrainAppSettings, 'settings') &&
    contains(
      options.JanrainAppSettings.settings,
      'tokenUrl',
      'httpLoadUrl',
      'httpsLoadUrl',
      'capture'
    ) &&
    contains(
      options.JanrainAppSettings.settings.capture,
      'clientId',
      'appId',
      'appDomain',
      'captureServer'
    )
  )
}

Upvotes: 3

ibrahim mahrir
ibrahim mahrir

Reputation: 31682

I would use a schema-like object that contains all the keys that need to exist in the object and recursively check if an object contains those keys or not using a function like so:

const verifyObject = (object, schema) =>
  Object.entries(schema).every(([key, value]) =>
    object && object[key] && (typeof value != "object" || verifyObject(object[key], value))
  );

The object[key] test will not only verify that the key exists but it will also verify that the value for that key is truthy which may fail the whole test if a value contains a falsy value. If you only want it to check if the key exists, then replace it with key in object.

The schema-like object for you JanrainAppSettings object is simply an object containing all the keys that needs to exists in a valid JanrainAppSettings, the values could be just 0s or empty strings as they are not needed:

const janrainAppSettingsSchema = { JanrainAppSettings: { settings: { capture: { clientId: "", appId: "", appDomain: "", captureServer: "" }, tokenUrl: "", httpLoadUrl: "", httpsLoadUrl: "" } } };

So to verify an object, just call verifyObject and pass the object and the schema-like object to verify against like so:

if(verifyObject(options, janrainAppSettingsSchema)) {
  // everything is ok
}

Demo:

const verifyObject = (object, schema) =>
  Object.entries(schema).every(([key, value]) =>
    object && object[key] && (typeof value != "object" || verifyObject(object[key], value))
  );



let thisWillPass = {
  JanrainAppSettings: {
    settings: {
      capture: {
        clientId: "Id",
        appId: "Id",
        appDomain: "Domain",
        captureServer: "Server"
      },
      tokenUrl: "Url",
      httpLoadUrl: "Url",
      httpsLoadUrl: "Url"
    }
  }
};

let thisWontPass = {
  JanrainAppSettings: {
    settings: {
      capture: {
        clientId: "Id",
        appId: "Id",
        // appDomain: "Domain",
        captureServer: "Server"
      },
      tokenUrl: "Url",
      httpLoadUrl: "Url",
      httpsLoadUrl: "Url"
    }
  }
};

const janrainAppSettingsSchema = { JanrainAppSettings: { settings: { capture: { clientId: "", appId: "", appDomain: "", captureServer: "" }, tokenUrl: "", httpLoadUrl: "", httpsLoadUrl: "" } } };

console.log("Will pass:", verifyObject(thisWillPass, janrainAppSettingsSchema));
console.log("Won't pass:", verifyObject(thisWontPass, janrainAppSettingsSchema));

Upvotes: 0

Mr. Polywhirl
Mr. Polywhirl

Reputation: 48590

You could create a helper function that takes a scope and var-arg properties.

const allSet = (scope, ...properties) =>
  scope && (!properties || properties.every(key => !key || key in scope))

It would check the scope, along with the presence of all the potential falsy properties.

const sampleOptions = {
  JanrainAppSettings: {
    settings: {
      tokenUrl: '$tokenUrl',
      capture: {
        clientId: '$clientId',
        appId: '$appId',
        appDomain: '$appDomain',
        captureServer: '$captureServer'
      },
      httpLoadUrl: '$httpLoadUrl',
      httpsLoadUrl: '$httpsLoadUrl'
    }
  }
}

const allSet = (scope, ...properties) =>
  scope && (!properties || properties.every(key => !key || key in scope))


const verifyJanrainAppSettings = (options) =>
  allSet(options, 'JanrainAppSettings') &&
  allSet(options.JanrainAppSettings, 'settings') &&
  allSet(options.JanrainAppSettings.settings, 'tokenUrl', 'httpLoadUrl', 'httpsLoadUrl') &&
  allSet(options.JanrainAppSettings.settings.capture, 'clientId', 'appId', 'captureServer')

console.log(verifyJanrainAppSettings(sampleOptions))

Upvotes: 0

Code-Apprentice
Code-Apprentice

Reputation: 83517

Perhaps something like if (['my', 'options'] in myObject) ...

This is the right idea. However, you need to loop over the array of strings and check if each of them is in myObject. You can do this loop with Array.every(). The only disadvantage is this only works at the deepest level. Since you have several levels of nesting here it will still get kind of ugly:

const verifyJanrainAppSettings = (options) => {
    return options.JanrainAppSettings
      && options.JanrainAppSettings.settings // dont' forget this check
      && ['tokenUrl', 'httpLoadUrl', 'httpsLoadUrl', 'capture'].every(key => // added options.JanrainAppSettings.settings.capture check
        key in options.JanrainAppSettings.settings
      )
      && ['clientId', 'appId', 'appDomain', 'captureServer'].every(key =>
        key in options.JanrainAppSettings.settings.capture
      );
}

Upvotes: 2

T.J. Crowder
T.J. Crowder

Reputation: 1073968

You can keep a reference to settings, which improves things a fair bit:

const verifyJanrainAppSettings = (options) => {
    const settings = options.JanrainAppSettings && options.JanrainAppSettings.settings;
    return settings &&
      settings.tokenUrl &&
      settings.capture && // ** This was missing in the question, but I think you need it
      settings.capture.clientId &&
      settings.capture.appId &&
      settings.capture.appDomain &&
      settings.capture.captureServer &&
      settings.httpLoadUrl &&
      settings.httpsLoadUrl;
};

While you could put some of those names in an array and use a loop, I think I'd leave it at that.

With optional chaining (new in ES2020), that first line could be:

    const settings = options.JanrainAppSettings?.settings;

The array version would look something like:

const settingsProps = "capture tokenUrl httpLoadUrl httpsLoadUrl".split(" ");
const captureProps = "clientId appId appDomain captureServer".split(" ");
const verifyJanrainAppSettings = (options) => {
    const settings = options.JanrainAppSettings && options.JanrainAppSettings.settings;
    return settings &&
      settingsProps.every(prop => settings[prop]) &&
      captureProps.every(prop => settings.capture[prop]);
};

adiga asks a good question. If you want existence rather than truthiness (you have truthiness in your question), you'll want in (or for an own property check, hasOwnProperty). For instance:

const settingsProps = "capture tokenUrl httpLoadUrl httpsLoadUrl".split(" ");
const captureProps = "clientId appId appDomain captureServer".split(" ");
const verifyJanrainAppSettings = (options) => {
    const settings = options.JanrainAppSettings && options.JanrainAppSettings.settings;
    return settings &&
      settingsProps.every(prop => prop in settings) &&
      captureProps.every(prop => prop in settings.capture);
};

Upvotes: 8

Related Questions