src/optic.js

  1. import { isFunction, isUndefined } from 'underscore';
  2. import BinderMixin from './binder_mixin.js';
  3. import { isLensClass } from '../src-cjs/constants.js';
  4. import { getIterator, handleNoniterableValue, isLens } from './utils.js';
  5. class Optic {
  6. [isLensClass] = true;
  7. /**
  8. * @class
  9. * @mixes BinderMixin
  10. */
  11. constructor() {}
  12. /* istanbul ignore next */
  13. /**
  14. * @abstract
  15. * @summary Get a combination of present and value of this slot
  16. * @param {*} subject The data to query
  17. * @param {...*} tail Additional subject for repeated application
  18. * @returns {Maybe.<*>} The value of the slot in a Maybe moand (*Nothing* if the slot is missing in *subject*)
  19. *
  20. * @description
  21. * If *tail* is given and getting this slot from from *subject* yields a
  22. * lens-like object (as indicated by a truthy `lens.isLens` property), then `#get_maybe()`
  23. * is called on that lens, passing the spread of *tail*. If the value of
  24. * this slot is *not* a lens, the result is an empty Object.
  25. */
  26. get_maybe() { abstractMethod(); }
  27. /**
  28. * @summary Get the value of this slot within subject data
  29. * @param {*} subject The data to query
  30. * @param {...*} tail Additional subjects for repeated application
  31. * @return {*} The value of this slot, or `undefined` if this slot is not present in *subject*
  32. *
  33. * @description
  34. * If *tail* is given, then `#get()` is called on the result of getting this
  35. * slot from *subject*, passing the spread of *tail*. This eliminates
  36. * repeated use of `.get` in code. The chaining fails, returning `undefined`,
  37. * if this slot in *subject* is not a lens-like object (as indicated by a truthy
  38. * `lens.isLens` property).
  39. */
  40. get(subject, ...tail) {
  41. const subjResult = this.get_maybe(subject).just;
  42. if (tail.length > 0) {
  43. return isLens(subjResult) ? subjResult.get(...tail) : undefined;
  44. }
  45. return subjResult;
  46. }
  47. /**
  48. * @summary Get the (iterable) value of this slot within some subject data
  49. * @param {*} subject The data to query
  50. * @param {Object} [options]
  51. * @param {*} [options.orThrow] {@link OptionalThrow} if the value of the slot exists but is not iterable
  52. * @return {Iterable.<*>} An iterable of values from this slot (or an empty Array)
  53. *
  54. * @description
  55. * If the slot does not exist within *subject*, this method returns an empty
  56. * Array. If the slot within *subject* contains a non-iterable value, this
  57. * method's behavior depends on *orThrow*. If *orThrow* is `undefined`, the
  58. * behavior is the same as if the slot did not exist: an empty Array is
  59. * returned. If *orThrow* is not `undefined`, *orThrow* is thrown; if
  60. * *orThrow* is an Object, its `noniterableValue` property will be set to the
  61. * slot's value before being thrown.
  62. *
  63. * This method differs from {@link Optic#get} in that the returned value will always
  64. * be iterable; thus, the return value of this method may safely be passed
  65. * into any function expecting an iterable value. One example usage is
  66. * constructing a `Seq` from the `immutable` package.
  67. *
  68. * Strings, though iterable, are considered scalar values; if the targeted
  69. * slot contains a string, the slot will be treated as non-iterable.
  70. */
  71. getIterable(subject, {orThrow} = {}) {
  72. const maybeVal = this.get_maybe(subject);
  73. if (getIterator(maybeVal.just)) {
  74. return maybeVal.just;
  75. } else {
  76. handleNoniterableValue(orThrow, maybeVal);
  77. return [];
  78. }
  79. }
  80. /**
  81. * @template T
  82. * @summary Conditionally evaluate functions depending on presence of slot
  83. * @param {*} subject The input structured data
  84. * @param {Object} branches
  85. * @param {function(*): T} [branches.then] Function evaluated if slot is present in *subject*
  86. * @param {function(): T} [branches.else] Function evaluated if slot is absent from *subject*
  87. * @returns {T} The value computed by *branches.then* or *branches.else*, or `undefined`
  88. *
  89. * @description
  90. * The presence of the slot determines whether *branches.then* or *branches.else*
  91. * is evaluated, with the result being returned from this method. If the
  92. * indicated property of *branches* is missing, then `undefined` is returned.
  93. */
  94. getting(subject, {then: thenDo, else: elseDo}) {
  95. const maybeVal = this.get_maybe(subject),
  96. handler = ('just' in maybeVal ? thenDo : elseDo) || (() => {});
  97. return handler.call(undefined, maybeVal.just);
  98. }
  99. /**
  100. * @summary Test for the presence of this slot in subject data
  101. * @param {*} subject The data to test
  102. * @return {Boolean} Whether this slot is present in *subject*
  103. */
  104. present(subject) {
  105. return 'just' in this.get_maybe(subject);
  106. }
  107. /* istanbul ignore next */
  108. /**
  109. * @template T
  110. * @summary Clone the input, transforming or deleting the Maybe value of this slot with a function
  111. * @param {T} subject The input structured data
  112. * @param {function(Maybe.<*>): Maybe.<*>} fn The function transforming the {@link Maybe} value of the slot
  113. * @return {T} A minimally changed clone of *subject* with this slot transformed per *fn*
  114. *
  115. * @description
  116. * The concept of this method is of applying some clear algorithm to the
  117. * content of the slot targeted by this Optic instance (in a {@link Maybe} monad,
  118. * to cover the possibility of the slot itself not existing in *subject*)
  119. * with the result returned in a {@link Maybe} monad (to allow for potential removal
  120. * of the slot).
  121. *
  122. * This implements what conceptually could, in Haskell, be described as:
  123. *
  124. * Json -> (Maybe Json -> Maybe Json) -> Json
  125. *
  126. * "Minimally changed" means that reference-copies are used wherever possible
  127. * while leaving subject unchanged, and that setting the slot to the strict-
  128. * equal value it already has results in returning *subject*.
  129. */
  130. xformInClone_maybe() { abstractMethod(); }
  131. /**
  132. * @template T
  133. * @summary Clone the input, transforming the value within this slot with a function
  134. * @param {T} subject The input structured data
  135. * @param {function(*): *} fn The function that transforms the slot value
  136. * @param {Object} [opts]
  137. * @param {Boolean} opts.addMissinng=false Whether to add the slot if missing in subject
  138. * @return {T} A minimally changed clone of *subject* with *fn* applied to the value in this slot
  139. *
  140. * @description
  141. * If this slot is missing in *subject*, *fn* will not be called unless *addMissing*
  142. * is true, in which case fn will be called with `undefined`.
  143. *
  144. * "Minimally changed" means that reference-copies are used wherever possible
  145. * while leaving subject unchanged, and that setting the slot to the strict-
  146. * equal value it already has results in returning *subject*.
  147. */
  148. xformInClone(subject, fn, {addMissing = false} = {}) {
  149. return this.xformInClone_maybe(subject, (value_maybe) => {
  150. if ('just' in value_maybe || addMissing) {
  151. return {just: fn(value_maybe.just)};
  152. } else {
  153. return {};
  154. }
  155. });
  156. }
  157. /**
  158. * @template T
  159. * @summary Clone the input, transforming the iterable value within this slot with a function
  160. * @param {T} subject The input structured data
  161. * @param {Function} fn The function that transforms the (iterable) slot value
  162. * @param {Object} [options]
  163. * @param {*} [options.orThrow] {@link OptionalThrow} if the value of the slot exists but is not iterable
  164. * @return {T} A minimally changed clone of subject with the transformed value in this slot
  165. *
  166. * @description
  167. * If the slot does not exist within *subject*, *fn* is invoked on an empty
  168. * Array. If the slot within *subject* contains a non-iterable value, this
  169. * method's behavior depends on *orThrow*. If *orThrow* is `undefined`, the
  170. * behavior is the same as if the slot did not exist: an empty Array is
  171. * passed to *fn*. If *orThrow* is not `undefined`, *orThrow* is thrown; if
  172. * *orThrow* is an Object, its `noniterableValue` property will be set to the
  173. * slot's value before being thrown.
  174. *
  175. * The primary differences between this method and {@link Lens#xformInClone} are that
  176. * this method always passes an iterable value to *fn* and always calls *fn*
  177. * even if the slot is missing or does not contain an iterable value (unless
  178. * *orThrow* is given).
  179. *
  180. * Strings, though iterable, are considered scalar values; if the targeted
  181. * slot contains a string, the slot will be treated as non-iterable.
  182. *
  183. * "Minimally changed" means that reference-copies are used wherever possible
  184. * while leaving *subject* unchanged, and that setting the slot to the strict-
  185. * equal value it already has results in returning *subject*.
  186. */
  187. xformIterableInClone(subject, fn, {orThrow} = {}) {
  188. return this.xformInClone_maybe(subject, maybeVal => {
  189. let input, result;
  190. if (getIterator(maybeVal.just)) {
  191. result = fn(input = maybeVal.just);
  192. } else {
  193. handleNoniterableValue(orThrow, maybeVal);
  194. input = [];
  195. if ('just' in maybeVal) {
  196. input.noniterableValue = maybeVal.just;
  197. }
  198. result = fn(input);
  199. }
  200. if (!getIterator(result)) {
  201. log({
  202. level: 'warn',
  203. message: "Noniterable result from fn of xformIterableInClone; substituting empty Array",
  204. subject,
  205. ...this,
  206. opticConstructor: this.constructor,
  207. input,
  208. fn,
  209. result,
  210. });
  211. return {just: []};
  212. }
  213. return {just: result};
  214. });
  215. }
  216. /**
  217. * @template T
  218. * @summary Clone *subject*, assigning the given value to this slot within the clone
  219. * @param {T} subject The input structured data
  220. * @param {*} newVal The new value to inject into the slot identified by this lens within the returned clone
  221. * @returns {T} A minimally changed clone of *subject* with *newVal* in this slot
  222. *
  223. * @description
  224. * Where {@link Optic#xformInClone_maybe} is about applying some algorithmic
  225. * transformation to the Maybe of this slot value, possibly setting or omitting
  226. * this slot from the resulting clone, this method is about creating a clone
  227. * returning the *newVal* when {@link Optic#get} of this instance is applied
  228. * to it. This base implementation suffices when the `xformInClone_maybe`
  229. * accepts as it's transform argument (i.e. second argument) a Function
  230. * returning a Maybe of the slot value.
  231. *
  232. * "Minimally changed" means that reference-copies are used wherever possible
  233. * while leaving *subject* unchanged, and that setting the slot to the strict-equal
  234. * value it already has results in returning *subject*.
  235. */
  236. setInClone(subject, newVal) {
  237. return this.xformInClone_maybe(subject, () => ({just: newVal}));
  238. }
  239. /**
  240. * @summary DRYly bind a Function to the Object from which it was obtained
  241. * @param {string|symbol} methodName
  242. * @param {Object} options
  243. * @param {*} options.on The subject of the Function binding; becomes *this* for the result
  244. * @param {boolean} [options.bindNow=false] Bind to the Optic's target within *on* rather than binding to *on*
  245. * @param {*} [options.orThrow] {@link OptionalThrow} if the slot referenced does not contain a Function; has precedence over *or*
  246. * @param {Function} [options.or] {@link FallbackBindingResult}, a Function to return if the slot referenced does not contain a Function
  247. * @returns {Function} A Function binding the *methodName* of the target of this optic to the target of this optic, or `function() {}` if no such function found
  248. * @see {@link Lens#bound}
  249. *
  250. * @description
  251. * This method is a way to avoid duplicating code referencing an object within
  252. * *options.on* when 1) obtaining the reference to the method's function,
  253. * and 2) binding that method to the object from which it was accessed.
  254. *
  255. * The return value of this method is *always* a Function; if the slot identified
  256. * by this optic is not present in *options.on* or does not host a method
  257. * named *methodName*, the trivial function (`function () {}` or equivalent)
  258. * will be returned.
  259. *
  260. * By default, the binding is lazy — the target of the lens within *on* is
  261. * evaluated when the resulting Function is invoked (though *on* itself is
  262. * bound in this call). To bind the resulting Function to its target
  263. * immediately when this method is called, set *options.bindNow* to `true`.
  264. *
  265. * @example
  266. * const data = {question: "What is the ultimate answer?"};
  267. * const qStarts = lens('question').binding('startsWith', {on: data});
  268. * qStarts('What') // => true
  269. * data.question = "Why is a raven like a writing desk?";
  270. * qStarts('What') // => false
  271. * qStarts('Why') // => true
  272. *
  273. * @example
  274. * const data = {question: "What is the ultimate answer?"};
  275. * const qStarts = lens('question').binding('startsWith', {on: data, bindNow: true});
  276. * qStarts('What') // => true
  277. * data.question = "Why is a raven like a writing desk?";
  278. * qStarts('What') // => true (because qStarts bound to the target of the lens when `binding` was called)
  279. */
  280. binding(methodName, {on, bindNow = false, orThrow, or}) {
  281. const optic = this;
  282. function lookUpPlayers() {
  283. const mSubj = optic.get(on), fn = (function() {
  284. try {return mSubj[methodName];} catch (e) {}
  285. }());
  286. return {mSubj, fn};
  287. }
  288. if (bindNow) {
  289. const {mSubj, fn} = lookUpPlayers();
  290. if (isFunction(fn)) {
  291. return fn.bind(mSubj);
  292. }
  293. if (orThrow) {
  294. throw orThrow;
  295. } else if (!isUndefined(or)) {
  296. return or;
  297. }
  298. return function() {};
  299. } else return function (...args) {
  300. const {mSubj, fn} = lookUpPlayers();
  301. if (isFunction(fn)) {
  302. return fn.apply(mSubj, args);
  303. }
  304. if (orThrow) {
  305. throw orThrow;
  306. } else if (!isUndefined(or)) {
  307. return or.apply(undefined, args);
  308. }
  309. };
  310. }
  311. }
  312. Object.assign(Optic.prototype, BinderMixin);
  313. export default Optic;
  314. /* istanbul ignore next */
  315. function abstractMethod() {
  316. throw "Abstract method not implemented by concrete class";
  317. }
  318. function log(info) {
  319. console[info.level || 'info'](info);
  320. }