From 7f0be9c88ef21abd577176d44050f6574c32fcdb Mon Sep 17 00:00:00 2001
From: Stefano Vozza <svozza@gmail.com>
Date: Fri, 4 Mar 2016 22:13:20 +0000
Subject: [PATCH] add prop function

---
 index.js      | 46 +++++++++++++++++++++++++++++------
 test/index.js | 67 ++++++++++++++++++++++++++++++++++++++++++++++++---
 2 files changed, 102 insertions(+), 11 deletions(-)

diff --git a/index.js b/index.js
index 6a74cac0..a2b7a640 100644
--- a/index.js
+++ b/index.js
@@ -353,6 +353,22 @@
                function() { return R.apply(f, R.prepend(this, arguments)); });
   };
 
+  //  prop :: Accessible a => String -> a -> b
+  var prop =
+  def('prop',
+      {a: [Accessible]},
+      [$.String, a, b],
+      function(key, obj) {
+        var boxed = Object(obj);
+        if (key in boxed) {
+          return boxed[key];
+        } else {
+          throw new TypeError('‘prop’ expected object to have a property ' +
+                              'named ‘' + key + '’; ' +
+                              R.toString(obj) + ' does not');
+        }
+      });
+
   //. ### Classify
 
   //# type :: a -> String
@@ -1024,7 +1040,7 @@
   method('Maybe#toBoolean',
          {},
          [$Maybe(a), $.Boolean],
-         R.prop('isJust'));
+         prop('isJust'));
 
   //# Maybe#toString :: Maybe a ~> String
   //.
@@ -1111,7 +1127,7 @@
   def('isNothing',
       {},
       [$Maybe(a), $.Boolean],
-      R.prop('isNothing'));
+      prop('isNothing'));
 
   //# isJust :: Maybe a -> Boolean
   //.
@@ -1128,7 +1144,7 @@
   def('isJust',
       {},
       [$Maybe(a), $.Boolean],
-      R.prop('isJust'));
+      prop('isJust'));
 
   //# fromMaybe :: a -> Maybe a -> a
   //.
@@ -1532,7 +1548,7 @@
   method('Either#toBoolean',
          {},
          [$Either(a, b), $.Boolean],
-         R.prop('isRight'));
+         prop('isRight'));
 
   //# Either#toString :: Either a b ~> String
   //.
@@ -1621,7 +1637,7 @@
   def('isLeft',
       {},
       [$Either(a, b), $.Boolean],
-      R.prop('isLeft'));
+      prop('isLeft'));
 
   //# isRight :: Either a b -> Boolean
   //.
@@ -1638,7 +1654,7 @@
   def('isRight',
       {},
       [$Either(a, b), $.Boolean],
-      R.prop('isRight'));
+      prop('isRight'));
 
   //# either :: (a -> c) -> (b -> c) -> Either a b -> c
   //.
@@ -1715,7 +1731,7 @@
   //. > S.encaseEither(S.I, JSON.parse, '[')
   //. Left(new SyntaxError('Unexpected end of input'))
   //.
-  //. > S.encaseEither(R.prop('message'), JSON.parse, '[')
+  //. > S.encaseEither(S.prop('message'), JSON.parse, '[')
   //. Left('Unexpected end of input')
   //. ```
   S.encaseEither =
@@ -2407,6 +2423,20 @@
 
   //. ### Object
 
+  //# prop :: Accessible a => String -> a -> b
+  //.
+  //. Takes a property name and an object with known properties and returns
+  //. the value of the specified property. If for some reason the object
+  //. lacks the specified property, a type error is thrown.
+  //.
+  //. For accessing properties of uncertain objects, use [`get`](#get) instead.
+  //.
+  //. ```javascript
+  //. > S.prop('a', {a: 1, b: 2})
+  //. 1
+  //. ```
+  S.prop = prop;
+
   //# get :: Accessible a => TypeRep b -> String -> a -> Maybe b
   //.
   //. Takes a [type representative](#type-representatives), a property
@@ -2417,7 +2447,7 @@
   //. The `Object` type representative may be used as a catch-all since most
   //. values have `Object.prototype` in their prototype chains.
   //.
-  //. See also [`gets`](#gets).
+  //. See also [`gets`](#gets) and [`prop`](#prop).
   //.
   //. ```javascript
   //. > S.get(Number, 'x', {x: 1, y: 2})
diff --git a/test/index.js b/test/index.js
index 5927decd..bd7721eb 100644
--- a/test/index.js
+++ b/test/index.js
@@ -2452,7 +2452,7 @@ describe('either', function() {
     });
 
     it('applies the first argument to the Error', function() {
-      eq(S.encaseEither(R.prop('message'), factorial, -1),
+      eq(S.encaseEither(S.prop('message'), factorial, -1),
          S.Left('Cannot determine factorial of negative number'));
     });
 
@@ -2512,7 +2512,7 @@ describe('either', function() {
     });
 
     it('applies the first argument to the Error', function() {
-      eq(S.encaseEither2(R.prop('message'), rem, 42, 0),
+      eq(S.encaseEither2(S.prop('message'), rem, 42, 0),
          S.Left('Cannot divide by zero'));
     });
 
@@ -2573,7 +2573,7 @@ describe('either', function() {
     });
 
     it('applies the first argument to the Error', function() {
-      eq(S.encaseEither3(R.prop('message'), area, 2, 2, 5),
+      eq(S.encaseEither3(S.prop('message'), area, 2, 2, 5),
          S.Left('Impossible triangle'));
     });
 
@@ -4105,6 +4105,67 @@ describe('list', function() {
 
 describe('object', function() {
 
+  describe('prop', function() {
+
+    it('is a binary function', function() {
+      eq(typeof S.prop, 'function');
+      eq(S.prop.length, 2);
+    });
+
+    it('type checks its arguments', function() {
+      assert.throws(function() { S.prop(1); },
+                    errorEq(TypeError,
+                            '‘prop’ expected a value of type String ' +
+                            'as its first argument; received 1'));
+
+      assert.throws(function() { S.prop('a', null); },
+                    errorEq(TypeError,
+                            '‘prop’ requires ‘a’ to implement Accessible; ' +
+                            'Null does not'));
+
+      assert.throws(function() { S.prop('a', true); },
+                    errorEq(TypeError,
+                            '‘prop’ expected object to have a property ' +
+                            'named ‘a’; true does not'));
+
+      assert.throws(function() { S.prop('a', 1); },
+                    errorEq(TypeError,
+                            '‘prop’ expected object to have a property ' +
+                            'named ‘a’; 1 does not'));
+    });
+
+    it('throws when the property is not present', function() {
+      assert.throws(function() { S.prop('map', 'abcd'); },
+                    errorEq(TypeError,
+                            '‘prop’ expected object to have a property ' +
+                            'named ‘map’; "abcd" does not'));
+
+      assert.throws(function() { S.prop('c', {a: 0, b: 1}); },
+                    errorEq(TypeError,
+                            '‘prop’ expected object to have a property ' +
+                            'named ‘c’; {"a": 0, "b": 1} does not'));
+
+      assert.throws(function() { S.prop('xxx', [1, 2, 3]); },
+                    errorEq(TypeError,
+                            '‘prop’ expected object to have a property ' +
+                            'named ‘xxx’; [1, 2, 3] does not'));
+    });
+
+    it('it returns the value of the specified object property', function() {
+      eq(S.prop('a', {a: 0, b: 1}), 0);
+      eq(S.prop('0', [1, 2, 3]), 1);
+      eq(S.prop('length', 'abc'), 3);
+      eq(S.prop('x', Object.create({x: 1, y: 2})), 1);
+      eq(S.prop('global', /x/g), true);
+    });
+
+    it('is curried', function() {
+      eq(S.prop('a').length, 1);
+      eq(S.prop('a')({a: 0, b: 1}), 0);
+    });
+
+  });
+
   describe('get', function() {
 
     it('is a ternary function', function() {