orbitjs / orbit

Composable data framework for ambitious web applications.
https://orbitjs.com
MIT License
2.33k stars 133 forks source link

Custom inflector with inversion? #851

Closed ef4 closed 3 years ago

ef4 commented 3 years ago

Upgrading to 0.17.beta, I need to reproduce the pluralization I had in 0.16 for (I think) JSONAPISerializers.ResourceTypePath and JSONAPISerializers.ResourceType. This nearly works:

import JSONAPISource, { JSONAPISerializers } from '@orbit/jsonapi';
import { buildSerializerSettingsFor } from '@orbit/serializers';

let inflectors = [
  'pluralize,'
  'dasherize',
];

new JSONAPISource(
  serializerSettingsFor: buildSerializerSettingsFor({
    settingsByType: {
      [JSONAPISerializers.ResourceTypePath]: {
        serializationOptions: {
          inflectors,
        },
      },
      [JSONAPISerializers.ResourceType]: {
        serializationOptions: {
          inflectors,
        },
      },
    },
  });
)

But the pluralization is naive, so I need to use buildInflector to extend it:

import { buildInflector } from '@orbit/serializers';
let inflectors = [
  buildInflector({ facility: 'facilities' }, (str) => `${str}s`),
  'dasherize',
];

And this works in the forward direction, but I lose inverse inflection that was working when I only had the default pluralize. Specifically, URLs and request bodies get pluralized, but then when deserializing the response I get "Model is not defined" errors because the JSON:API type in the response has not been singularized.

How can I get both custom pluralization and inversion?

dgeb commented 3 years ago

@ef4 You are indeed very close. Here's a similar snippet I've shared on gitter:

import { buildSerializerSettingsFor, buildInflector } from '@orbit/serializers';

let JSONAPISettings = {
  serializerSettingsFor: buildSerializerSettingsFor({
    sharedSettings: {
      inflectors: {
        pluralize: buildInflector(
          { cow: 'kine', person: 'people' }, // custom mappings
          (input) => `${input}s` // naive pluralizer, specified as a fallback
        ),
        singularize: buildInflector(
          { kine: 'cow', people: 'person' }, // custom mappings
          (arg) => arg.substr(0, arg.length - 1) // naive singularizer, specified as a fallback
        )
      }
    },
    settingsByType: {
      [JSONAPISerializers.ResourceField]: {
        serializationOptions: { inflectors: ['dasherize'] }
      },
      [JSONAPISerializers.ResourceFieldParam]: {
        serializationOptions: { inflectors: ['dasherize'] }
      },
      [JSONAPISerializers.ResourceFieldPath]: {
        serializationOptions: { inflectors: ['dasherize'] }
      },
      [JSONAPISerializers.ResourceType]: {
        serializationOptions: { inflectors: ['pluralize', 'dasherize'] }
      },
      [JSONAPISerializers.ResourceTypePath]: {
        serializationOptions: { inflectors: ['pluralize', 'dasherize'] }
      }
    }
  })
};

The key here is that standard serializers know about certain inverse inflectors that are identified by name. The string serializer knows that the inverse of pluralize is singularize for instance.

By defining our custom inflector by name in sharedSettings, these can be shared across all serializers. And you can continue to reference those inflectors by name in your serializaitonOptions.

IMO one awkward piece here is the manual inversion of the custom mappings, which certainly could be handled in a more DRY and clever way.

dgeb commented 3 years ago

To follow up, something along these lines keeps the custom mappings more DRY:

import { buildSerializerSettingsFor, buildInflector } from '@orbit/serializers';

const pluralizations = { cow: 'kine', person: 'people' };
const singularizations = Object.keys(pluralizations).reduce((inverse, k) => { inverse[pluralizations[k]] = k; return inverse; }, {});

let JSONAPISettings = {
  serializerSettingsFor: buildSerializerSettingsFor({
    sharedSettings: {
      inflectors: {
        pluralize: buildInflector(
          pluralizations,
          (input) => `${input}s` // naive pluralizer, specified as a fallback
        ),
        singularize: buildInflector(
          singularizations,
          (arg) => arg.substr(0, arg.length - 1) // naive singularizer, specified as a fallback
        )
      }
    },
    settingsByType: {
      [JSONAPISerializers.ResourceField]: {
        serializationOptions: { inflectors: ['dasherize'] }
      },
      [JSONAPISerializers.ResourceFieldParam]: {
        serializationOptions: { inflectors: ['dasherize'] }
      },
      [JSONAPISerializers.ResourceFieldPath]: {
        serializationOptions: { inflectors: ['dasherize'] }
      },
      [JSONAPISerializers.ResourceType]: {
        serializationOptions: { inflectors: ['pluralize', 'dasherize'] }
      },
      [JSONAPISerializers.ResourceTypePath]: {
        serializationOptions: { inflectors: ['pluralize', 'dasherize'] }
      }
    }
  })
};
ef4 commented 3 years ago

Thanks. This looks like it will solve my problem.

Totally as an aside, one of my favorite little new ES feature is how Object.fromEntries can make this clearer:

const singularizations = Object.fromEntries(Object.entries(pluralizations).map(([k,v]) => [v,k]))
dgeb commented 3 years ago

Ah yes, that is sooo much clearer!