😞

😞

ごめんăȘさい

2067

2067

Elm, OG8, Price Offers, Proof of Concepts

and other relatively accurate stories
gizra // @amitaibu

The Gizra stack

  • Drupal
  • Elm
  • Yesod (Haskell)

Clients pay for the features

not the bugs

Articulate the problem

It's not about fighting bugs. It's about setting our priorities

Correctness

and my expectations from computers

Elm

Generate JavaScript with great performance and no runtime exceptions

module Counter exposing (..)

import Html exposing (..)
import Html.Attributes exposing (..)
import Html.Events exposing (onClick)

-- MODEL

type alias Model =
    Int

emptyModel : Model
emptyModel =
    0

-- UPDATE

type Msg
    = Decrement
    | Increment


update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        Decrement ->
            ( model - 1
            , Cmd.none
            )

        Increment ->
            ( model + 1
            , Cmd.none
            )

-- VIEW

view : Model -> Html Msg
view model =
    div []
        [ button [ onClick Decrement ] [ text "-" ]
        , div [] [ text (toString model) ]
        , button [ onClick Increment ] [ text "+" ]
        ]

        
          
--

type alias User =
    { avatarUrl : String
    , name : String
    }

viewAvatar : String -> Html a
viewAvatar url =
    img [ src url ] []


--
          
        
          
--

type alias User =
    { avatarUrl : String
    , name : String
    }

viewAvatar : String -> Html a
viewAvatar url =
    img [ src url ] []


main = viewAvatar "But this isn't a URL!"
          
        

Compile Error Vs Runtime Mistakes

Types 101

          
type Bool = False | True
          
        
          
type UserType = Anonymous | Authenticated String | Premium String Date
          
        
          
--

type alias User =
    { avatarUrl : String
    , name : String
    }

viewAvatar : String -> Html a
viewAvatar url =
    img [ src url ] []


--
          
        
          
type Url = Url String

type alias User =
    { avatarUrl : Url
    , name : String
    }

viewAvatar : Url -> Html a
viewAvatar url =
    let
        (Url val) = url
    in
        img [ src val ] []
          
        

Maybe values

!empty(), you will not be missed
          
type alias User =
    { avatarUrl : Url
    , name : String
    }

emptyUser =
    { avatarUrl = Url ""
    , name = ""
    }

type alias Model =
    { user : User
    }

emptyModel =
    { user = emptyUser
    }
          
        
          
type alias Model =
    { user : Maybe User
    }

emptyModel =
    { user = Nothing
    }
          
        
          

type alias Model =
    { user : Maybe User
    }

emptyModel =
    { user = Just <| User "https://example.com/avatar" "amitaibu"
    }
          
        
          
{-|
  * `NotAsked` - We haven't asked for the data yet.
  * `Loading` - We've asked, but haven't got an answer yet.
  * `Failure` - We asked, but something went wrong. Here's the error.
  * `Success` - Everything worked, and here's the data.
-}
type RemoteData e a
    = NotAsked
    | Loading
    | Failure e
    | Success a

type alias WebData a =
    RemoteData Http.Error a
          
        
          
type alias Model =
    { user : WebData User
    }


emptyModel =
    { user = NotAsked
    }

debugUser =
    { user = Success <| User "https://example.com/avatar" "amitaibu"
    }
          
        
          
type Language
    = English
    | Spanish


type TranslationId
    = Login
    | WelcomeBack { name : String }


type alias TranslationSet =
    { english : String
    , spanish : String
    }


translations : TranslationId -> TranslationSet
translations id =
    case id of
        Login ->
            { english = "Please login"
            , spanish = "Por favor haga login"
            }

        WelcomeBack val ->
            { english = "Welcome back " ++ val.name
            , spanish = "Bienvenido " ++ val.name
            }


translate : Language -> TranslationId -> String
translate lang id =
    case lang of
        English ->
            .english <| translations id

        Spanish ->
            .spanish <| translations id

          
        
What can possibly go wrong?
                    
drupal_add_css('static/css/bootstrap.css');
                    
                
                    
addStylesheet $ StaticR css_bootstrap_css
                    
                

Finally! Drupal (8)!

OG8

Safe Guards

                  
        throw new \Exception();
                  

                

Be your own devil's advocate

  • Functional testing
  • Kernel testing
  • Unit testing
          
/**
 * Tests the formatter changes by user and membership.
 */
public function testGroupTab() {
  $this->drupalLogin($this->user1);
  $this->drupalGet('group/node/' . $this->group->id() . '/admin');
  $this->assertResponse(200);

  $this->drupalGet('group/node/' . $this->nonGroup->id() . '/admin');
  $this->assertResponse(403);
}
          
        
          
/**
 * Tests access to the create entity form through the user interface.
 *
 * @group og
 * @coversDefaultClass \Drupal\og\Form\GroupSubscribeForm
 */
class GroupSubscribeFormTest extends KernelTestBase {

  /**
   * {@inheritdoc}
   */
  public static $modules = [
    'system',
    'user',
    'field',
    'node',
    'og',
  ];

  /**
   * A user object.
   *
   * @var \Drupal\user\Entity\User
   */
  protected $user1;


  /**
   * A group entity.
   *
   * @var \Drupal\node\Entity\Node
   */
  protected $group1;

  /**
   * A group entity.
   *
   * @var \Drupal\node\Entity\Node
   */
  protected $group2;

  /**
   * A group entity.
   *
   * @var \Drupal\node\Entity\Node
   */
  protected $group3;

  /**
   * {@inheritdoc}
   */
  protected function setUp() {
    parent::setUp();

    $this->installConfig(['og']);
    $this->installEntitySchema('og_membership');
    $this->installEntitySchema('user');
    $this->installEntitySchema('node');
    $this->installSchema('system', 'sequences');

    // Create bundles.
    $groupBundle1 = Unicode::strtolower($this->randomMachineName());
    $groupBundle2 = Unicode::strtolower($this->randomMachineName());
    $groupBundle3 = Unicode::strtolower($this->randomMachineName());

    // Define the entities as groups.
    Og::groupTypeManager()->addGroup('node', $groupBundle1);
    Og::groupTypeManager()->addGroup('node', $groupBundle2);
    Og::groupTypeManager()->addGroup('node', $groupBundle3);

    // Create node author user.
    $user = User::create(['name' => $this->randomString()]);
    $user->save();

    // Create groups.
    $this->group1 = Node::create([
      'type' => $groupBundle1,
      'title' => $this->randomString(),
      'uid' => $user->id(),
    ]);
    $this->group1->save();

    $this->group2 = Node::create([
      'type' => $groupBundle2,
      'title' => $this->randomString(),
      'uid' => $user->id(),
    ]);
    $this->group2->save();

    // Create an unpublished node, so users won't have access to it.
    $this->group3 = Node::create([
      'type' => $groupBundle3,
      'title' => $this->randomString(),
      'uid' => $user->id(),
      'status' => NODE_NOT_PUBLISHED,
    ]);
    $this->group3->save();

    // Change the permissions of group to "subscribe".
    /** @var OgRole $role */
    $role = OgRole::getRole('node', $groupBundle1, OgRoleInterface::ANONYMOUS);
    $role
      ->grantPermission('subscribe')
      ->save();

    // Change the permissions of group to allow "subscribe without approval".
    $role = OgRole::getRole('node', $groupBundle2, OgRoleInterface::ANONYMOUS);
    $role
      ->grantPermission('subscribe without approval')
      ->save();

    // Change the permissions of group to allow "subscribe without approval" on
    // the unpublished node.
    $role = OgRole::getRole('node', $groupBundle3, OgRoleInterface::ANONYMOUS);
    $role
      ->grantPermission('subscribe without approval')
      ->save();

  }

  /**
   * Tests subscribe confirmation related text.
   *
   * @covers ::isStateActive
   */
  public function testIsStateActive() {
    $user = $this->createUser(['access content']);

    /** @var GroupSubscribeForm $form */
    $form = \Drupal::entityManager()->getFormObject('og_membership', 'subscribe');

    // Pending membership.
    $membership_pending = OgMembership::create();
    $membership_pending
      ->setGroup($this->group1)
      ->setUser($user);

    $form->setEntity($membership_pending);
    $this->assertFalse($form->isStateActive());
  }
          
        
          
/**
 * Tests the group check access.
 *
 * @group og
 * @coversDefaultClass \Drupal\og\Access\GroupCheck
 */
class GroupCheckTest extends UnitTestCase {

  /**
   * The entity type manager prophecy used in the test.
   *
   * @var \Drupal\Core\Entity\EntityTypeManagerInterface|\Prophecy\Prophecy\ObjectProphecy
   */
  protected $entityTypeManager;

  /**
   * The entity type prophecy used in the test.
   *
   * @var \Drupal\Core\Entity\EntityTypeInterface|\Prophecy\Prophecy\ObjectProphecy
   */
  protected $entityType;

  /**
   * The entity storage prophecy used in the test.
   *
   * @var \Drupal\Core\Entity\EntityStorageInterface|\Prophecy\Prophecy\ObjectProphecy
   */
  protected $entityStorage;

  /**
   * The OG access service prophecy used in the test.
   *
   * @var \Drupal\og\OgAccess|\Prophecy\Prophecy\ObjectProphecy
   */
  protected $ogAccess;

  /**
   * The route service prophecy used in the test.
   *
   * @var \Symfony\Component\Routing\Route|\Prophecy\Prophecy\ObjectProphecy
   */
  protected $route;

  /**
   * A user used in the test.
   *
   * @var \Drupal\user\UserInterface|\Prophecy\Prophecy\ObjectProphecy
   */
  protected $user;

  /**
   * The entity type ID of the test group.
   *
   * @var string
   */
  protected $entityTypeId;

  /**
   * The bundle ID of the test group.
   *
   * @var string
   */
  protected $bundle;

  /**
   * The test group entity used in the test..
   *
   * @var \Drupal\Core\Entity\EntityInterface|\Prophecy\Prophecy\ObjectProphecy
   */
  protected $group;

  /**
   * A random entity ID.
   *
   * @var int
   */
  protected $entityId;

  /**
   * The group manager used in the test.
   *
   * @var \Drupal\og\GroupTypeManager|\Prophecy\Prophecy\ObjectProphecy
   */
  protected $groupTypeManager;

  /**
   * The access result used in the test.
   *
   * @var \Drupal\Core\Access\AccessResultInterface|\Prophecy\Prophecy\ObjectProphecy
   */
  protected $accessResult;

  /**
   * The route match service used in the test.
   *
   * @var \\Drupal\Core\Routing\RouteMatchInterface|\Prophecy\Prophecy\ObjectProphecy
   */
  protected $routeMatch;

  /**
   * {@inheritdoc}
   */
  public function setUp() {
    $this->entityTypeManager = $this->prophesize(EntityTypeManagerInterface::class);
    $this->entityType = $this->prophesize(EntityTypeInterface::class);
    $this->entityStorage = $this->prophesize(EntityStorageInterface::class);
    $this->ogAccess = $this->prophesize(OgAccessInterface::class);
    $this->route = $this->prophesize(Route::class);
    $this->routeMatch = $this->prophesize(RouteMatchInterface::class);

    $this->entityTypeId = $this->randomMachineName();
    $this->bundle = $this->randomMachineName();
    $this->entityId = rand(10, 50);
    $this->groupTypeManager = $this->prophesize(GroupTypeManager::class);
    $this->user = $this->prophesize(AccountInterface::class);
    $this->group = $this->prophesize(EntityInterface::class);
    $this->accessResult = $this->prophesize(AccessResultInterface::class);

    $container = new ContainerBuilder();
    $container->set('og.group_type_manager', $this->groupTypeManager->reveal());
    \Drupal::setContainer($container);
  }

  /**
   * Tests a non-existing group.
   *
   * @covers ::access
   */
  public function testNoGroup() {
    $this
      ->entityTypeManager
      ->getDefinition($this->entityTypeId, FALSE)
      ->willReturn($this->entityType);

    $this
      ->entityTypeManager
      ->getStorage($this->entityTypeId)
      ->willReturn($this->entityStorage);

    $this->entityStorage
      ->load($this->entityId)
      ->willReturn(NULL);

    $group_check = new GroupCheck($this->entityTypeManager->reveal(), $this->ogAccess->reveal());
    $result = $group_check->access($this->user->reveal(), $this->route->reveal(), $this->routeMatch->reveal(), $this->entityTypeId, $this->entityId);

    $this->assertTrue($result->isForbidden());
  }
}
          
        

Port Drupal to Haskell?

no!
gizra