Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

inspect #2038

Closed
icylace opened this issue Jan 3, 2017 · 7 comments
Closed

inspect #2038

icylace opened this issue Jan 3, 2017 · 7 comments

Comments

@icylace
Copy link

icylace commented Jan 3, 2017

I've created a function called inspect which will gather values from a data structure according to a given spec and will return those values as an object keyed according to strings in the spec.

I'm not sure if it's more appropriate for this to be added to the Ramda cookbook or if it's useful enough to be considered for inclusion in Ramda itself. Let me know what you think !

var inspect = R.curry(function _inspect(spec, obj) {
  var props = {}
  function inspectProps(spec, obj) {
    R.forEachObjIndexed(function _inspectProp(value, key) {
      var objValue = obj[key]
      if (typeof value === "string" && typeof objValue !== "undefined") {
        props[value] = objValue
      } else if (typeof objValue === "object") {
        inspectProps(value, objValue)
      }
    }, spec)
  }
  inspectProps(spec, obj)
  return props
})

Example Usage

// Strings in our spec serve two purposes:
//   1. As markers for which parts of our data structure to extract.
//   2. As the key names in our returned object which contain the data gathered.
var spec = {
  a: {
    h: [
      null,
      [
        { j: "foo" },
        "bar"
      ],
      "baz"
    ]
  }
}

// Note that our data structure must "line-up" with the spec for things to work.
var data1 = {
  a: {
    h: [
      { i: 5 },
      [
        { j: 6, k: 7 },
        { j: 8, k: "nine" }
      ],
      10
    ]
  }
}

// When data can't be extracted due to absence the returned object will
// simply omit the associated keys for the missing sections.
var data2 = {
  a: {
    h: [
      { i: 5 },
      [
        { j: 8, k: "nine" }
      ]
    ]
  }
}

var inspector = inspect(spec)

console.log(inspector(data1))
// Output will be:
// {
//   "foo": 6,
//   "bar": {
//     "j": 8,
//     "k": "nine"
//   },
//   "baz": 10
// }

console.log(inspector(data2))
// Output will be:
// {
//   "foo": 8
// }

See it in action here:
http://codepen.io/icylace/pen/jyOEEb?editors=1012

@CrossEye
Copy link
Member

CrossEye commented Jan 3, 2017

I'd love to see something more real-world that motivated a function like this. I don't think I've had any real call for it, but perhaps I'd recognize it with a more realistic scenario.

@icylace
Copy link
Author

icylace commented Jan 3, 2017

Certainly! Please forgive the long backstory behind this function. :)

I recently searched for a configuration wizard for ESLint. I've only found one that already exists but it's out-of-date, doesn't have the ability to set all the rule-specific configurations, and its UI leaves a lot to be desired. So, I took it upon myself to make a better one.

Since there are well over 200 rules as of ESLint 3.12.2, it's desirable to automate the creation of a configuration wizard UI as much as possible. Fortunately, each of ESLint's rules has their schema consistently defined using JSON Schema. The basic idea is to of course leverage this metadata to drive the creation of form controls for the wizard interface.

There are a couple issues with this:

  1. Not all rule-specific metadata are encoded in the rule's schema (e.g. default values) even though they're expressed in the rule's documentation.
  2. The JSON Schema format was designed for structural validation of JSON data. It wasn't designed to aid in the generation of UI components.

The first issue will likely require manual effort to address but the second issue is where things get interesting.

My goal for the second issue was to programmatically convert each rule's schema into an internal custom format that I believe will be easier to deal with for my purposes. My naive initial attempt at this was to do a rote translation of the rule settings into their comparable JavaScript forms.

To illustrate with a basic example, let's look at the schema for the func-style rule:

[
  { "enum": ["declaration", "expression"] },
  {
    "type": "object",
    "properties": {
      "allowArrowFunctions": {
        "type": "boolean"
      }
    },
    "additionalProperties": false
  }
]

My naive method simplified this to:

[
  ["declaration", "expression"],
  {
    "props": {
      "allowArrowFunctions": false
    }
  }
]

So, my naive method worked pretty well for the majority of the rule schemas but there were some that required additional simplification steps. One such schema was for eqeqeq:

{
  "anyOf": [
    {
      "type": "array",
      "items": [
        { "enum": ["always"] },
        {
          "type": "object",
          "properties": {
            "null": { "enum": ["always", "never", "ignore"] }
          },
          "additionalProperties": false
        }
      ],
      "additionalItems": false
    },
    {
      "type": "array",
      "items": [{ "enum": ["smart", "allow-null"] }],
      "additionalItems": false
    }
  ]
}

My naive method produced:

[
  {
    "anyOf": [
      {
        "array": {
          "choices": [
            ["always"],
            {
              "props": {
                "null": ["always", "never", "ignore"]
              }
            }
          ],
          "moreItemsNotAllowed": true
        }
      },
      {
        "array": {
          "choices": [["smart", "allow-null"]],
          "moreItemsNotAllowed": true
        }
      }
    ]
  }
]

But this wasn't sufficient. What I actually wanted, and what I eventually achieved using a second pass of simplification, was this:

[
  ["always", "smart", "allow-null"],
  {
    "props": {
      "null": ["always", "never", "ignore"]
    },
    "availableFor": ["always"]
  }
]

I'll spare you the details of how I did this and other similar conversions but suffice it to say I felt my code could be better.

I then took a step back, thought about it for a while, and came up with the idea of generalizing the pattern matching aspects of what I was doing and placing that into a new helper function that can do this sort of declarative multi-selector type of thing.

This is how inspect came to be.

Once I created this function I applied it to my project in a few key places. For instance, here's a code snippet partially showing how I solved the aforementioned eqeqeq case:

const spec = {
  anyOf: [
    {
      array: {
        choices: ["primary1", { props: "secondary" }],
      },
    },
    {
      array: {
        choices: ["primary2"],
      },
    },
  ],
}
const inspectee = inspect(spec, schema)
return [
  uniq(concat(inspectee.primary1, inspectee.primary2)),
  {
    props: inspectee.secondary,
    availableFor: inspectee.primary1,
  },
]

To me this was clearer than what I would have otherwise done. As an added bonus, I started seeing potential opportunities for refactoring that I had not noticed before using inspect.

In other words, inspect made my life a whole lot easier !

@CrossEye
Copy link
Member

CrossEye commented Jan 3, 2017

Thank you. This is a fascinating story, and a very interesting function.

My take is that for the moment, the best place is the Cookbook. If we find ourselves recommending it more than every few months, then we could consider moving it into the main library. Does that sound reasonable?

@icylace
Copy link
Author

icylace commented Jan 3, 2017

Sounds good !

Hopefully this function will save other developers' time.

@kedashoe
Copy link
Contributor

kedashoe commented Jan 3, 2017

Great post @icylace , thank you and look forward to seeing this in the cookbook :)

@icylace
Copy link
Author

icylace commented Jan 3, 2017

@CrossEye As you suggested, I added my inspect function to the Cookbook: https://github.com/ramda/ramda/wiki/Cookbook#use-a-spec-to-get-some-parts-of-a-data-structure

@icylace icylace closed this as completed Jan 3, 2017
@CrossEye
Copy link
Member

CrossEye commented Jan 4, 2017

@icylace:

Thank you very much. I can well imagine using this function. I'm glad to know it's handy if the time comes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants