<commit_before>/**
 *
 * weatherunderground adapter
 *
 * Adapter loading the json forecast of weatherunderground
 *
 * note: you need an account and an api key to get the forecast. This is free for non excess usage: 500 requests/d
 * see: http://www.wunderground.com/weather/api/d/pricing
 *
 * register for a key:
 * http://www.wunderground.com/weather/api/d/questionnaire.html?plan=a&level=0&history=undefined
 *
 * see http://www.wunderground.com/weather/api/d/docs?d=data/hourly
 * for reference of the possible values from hourly WU forecast
 *
 */

"use strict";

var utils = require(__dirname + '/lib/utils'); // Get common adapter utils

var request = require('request');

var adapter = utils.adapter({
    name: 'weatherunderground',

    unload: function (callback) {
        adapter.log.info("adapter weatherunderground is unloading");
    },
    discover: function (callback) {
    },
    install: function (callback) {
        adapter.log.info("adapter weatherunderground installed");
    },
    uninstall: function (callback) {
        adapter.log.info("adapter weatherunderground UN-installed");
    },
    objectChange: function (id, obj) {
        adapter.log.info('objectChange ' + id + ' ' + JSON.stringify(obj));
    },
    stateChange: function (id, state) {
        adapter.log.info('stateChange ' + id + ' ' + JSON.stringify(state));
    },
    ready: function () {
        adapter.log.info("Adapter weatherunderground got 'Ready' Signal - starting scheduler to look for forecasts");
        checkWeatherVariables();
        getWuForecastData();
    },
    message: function (obj) {
    }
});

function getWuForecastData() {

    //debug

    var url = "http://api.wunderground.com/api/" + adapter.config.apikey + "/hourly/q/" + adapter.config.location + ".json";

    request({url: url, json: true}, function(error, response, body) {
        if (!error && response.statusCode === 200) {
            var qpf_sum = 0;
            var pop_max = 0;
            var uvi_sum = 0;

            for (var i = 0; i < 24; i++) {
                try {
                    //adapter.log.info("WU-Response Body: " + JSON.stringify(body.hourly_forecast[0].FCTTIME)); // Print the json response
                    //var forecast = JSON.parse(body.jsonData);
                    //adapter.log.info("version: " + body.response.version);
                    // see http://www.wunderground.com/weather/api/d/docs?d=resources/phrase-glossary for infos about properties and codes
                    adapter.setState("forecast." + i + "h.time", {ack: true, val: body.hourly_forecast[i].FCTTIME.pretty});
                    adapter.setState("forecast." + i + "h.temp", {ack: true, val: body.hourly_forecast[i].temp.metric});
                    adapter.setState("forecast." + i + "h.fctcode", {ack: true, val: body.hourly_forecast[i].fctcode}); //forecast description number -> see link above
                    adapter.setState("forecast." + i + "h.sky", {ack: true, val: body.hourly_forecast[i].sky}); //?
                    adapter.setState("forecast." + i + "h.wspd", {ack: true, val: body.hourly_forecast[i].wspd.metric}); // windspeed in kmh
                    adapter.setState("forecast." + i + "h.wdir", {ack: true, val: body.hourly_forecast[i].wdir.degrees}); //wind dir in degrees
                    adapter.setState("forecast." + i + "h.uvi", {ack: true, val: body.hourly_forecast[i].uvi}); //UV Index -> wikipedia
                    adapter.setState("forecast." + i + "h.humidity", {ack: true, val: body.hourly_forecast[i].humidity});
                    adapter.setState("forecast." + i + "h.heatindex", {ack: true, val: body.hourly_forecast[i].heatindex.metric}); // -> wikipedia
                    adapter.setState("forecast." + i + "h.feelslike", {ack: true, val: body.hourly_forecast[i].feelslike.metric}); // -> wikipedia
                    adapter.setState("forecast." + i + "h.qpf", {ack: true, val: body.hourly_forecast[i].qpf.metric}); // Quantitative precipitation forecast
                    adapter.setState("forecast." + i + "h.snow", {ack: true, val: body.hourly_forecast[i].snow.metric});
                    adapter.setState("forecast." + i + "h.pop", {ack: true, val: body.hourly_forecast[i].pop}); // probability of Precipitation
                    adapter.setState("forecast." + i + "h.mslp", {ack: true, val: body.hourly_forecast[i].mslp.metric}); // mean sea level pressure

                    qpf_sum += Number(body.hourly_forecast[i].qpf.metric);
                    uvi_sum += Number(body.hourly_forecast[i].uvi);
                    if (Number(body.hourly_forecast[i].uvi) > pop_max) {
                        pop_max = Number(body.hourly_forecast[i].uvi);
                    }

                    // 6h
                    if (i == 5) {
                        adapter.setState("forecast.6h.sum.qpf", {ack: true, val: qpf_sum});
                        adapter.setState("forecast.6h.sum.pop", {ack: true, val: pop_max});
                        adapter.setState("forecast.6h.sum.uvi", {ack: true, val: uvi_sum/6});
                    }
                    // 12h
                    if (i == 11) {
                        adapter.setState("forecast.12h.sum.qpf", {ack: true, val: qpf_sum});
                        adapter.setState("forecast.12h.sum.pop", {ack: true, val: pop_max});
                        adapter.setState("forecast.12h.sum.uvi", {ack: true, val: uvi_sum/12});
                    }
                    // 24h
                    if (i == 23) {
                        adapter.setState("forecast.24h.sum.qpf", {ack: true, val: qpf_sum});
                        adapter.setState("forecast.24h.sum.pop", {ack: true, val: pop_max});
                        adapter.setState("forecast.24h.sum.uvi", {ack: true, val: uvi_sum/24});
                    }
                } catch (error) {
                    adapter.log.error("Could not parse Forecast-Data: " + error);
                }
            }
            adapter.log.info("all forecast values set");
        } else
        {
            // ERROR
            adapter.log.error("Wunderground reported an error: " + error);
        }
    });
}

function checkWeatherVariables() {
    adapter.log.info("init forecast variables", 'info');

    ['time','temp','fctcode','sky','wspd','wdir','uvi','humidity','heatindex','feelslike','qpf','snow','pop','mslp'].map( function(v) {
        for (var i = 0; i < 24; i++) {
            var name = "forecast." + i + "h." + v;
            if (adapter.getState(name) === null) {
                var obj = {
                    type: 'state',
                    common: {name: v, read: true, write: true, type: 'number'},
                    native: {id: name}
                };
                adapter.setObjectNotExists(name, obj);
                adapter.log.info("created object: " + name);
            }
        }
    });
    ['forecast.6h.sum.qpf','forecast.12h.sum.qpf','forecast.24h.sum.qpf',
        'forecast.6h.sum.pop', 'forecast.12h.sum.pop', 'forecast.24h.sum.pop',
        'forecast.6h.sum.uvi','forecast.12h.sum.uvi','forecast.24h.sum.uvi'].map( function(name) {
            if (adapter.getState(name) === null) {
                var obj = {
                    type: 'state',
                    common: {name: name, read: true, write: true, type: 'number'},
                    native: {id: name}
                };
                adapter.setObjectNotExists(name, obj);
            }
        });
}

<commit_msg>initial version<commit_after>/**
 *
 * weatherunderground adapter
 *
 * Adapter loading the json forecast of weatherunderground
 *
 * note: you need an account and an api key to get the forecast. This is free for non excess usage: 500 requests/d
 * see: http://www.wunderground.com/weather/api/d/pricing
 *
 * register for a key:
 * http://www.wunderground.com/weather/api/d/questionnaire.html?plan=a&level=0&history=undefined
 *
 * see http://www.wunderground.com/weather/api/d/docs?d=data/hourly
 * for reference of the possible values from hourly WU forecast
 *
 */

"use strict";

var utils = require(__dirname + '/lib/utils'); // Get common adapter utils

var request = require('request');

var adapter = utils.adapter({
    name: 'weatherunderground',

    unload: function (callback) {
        adapter.log.info("adapter weatherunderground is unloading");
    },
    discover: function (callback) {
    },
    install: function (callback) {
        adapter.log.info("adapter weatherunderground installed");
    },
    uninstall: function (callback) {
        adapter.log.info("adapter weatherunderground UN-installed");
    },
    objectChange: function (id, obj) {
        adapter.log.info('objectChange ' + id + ' ' + JSON.stringify(obj));
    },
    stateChange: function (id, state) {
        adapter.log.info('stateChange ' + id + ' ' + JSON.stringify(state));
    },
    ready: function () {
        adapter.log.info("Adapter weatherunderground got 'Ready' Signal - starting scheduler to look for forecasts");
        checkWeatherVariables();
        getWuForecastData();
    },
    message: function (obj) {
    }
});

function getWuForecastData() {

    //debug

    var url = "http://api.wunderground.com/api/" + adapter.config.apikey + "/hourly/q/" + adapter.config.location + ".json";

    request({url: url, json: true}, function(error, response, body) {
        if (!error && response.statusCode === 200) {
            var qpf_sum = 0;
            var pop_max = 0;
            var uvi_sum = 0;

            for (var i = 0; i < 24; i++) {
                try {
                    //adapter.log.info("WU-Response Body: " + JSON.stringify(body.hourly_forecast[0].FCTTIME)); // Print the json response
                    //var forecast = JSON.parse(body.jsonData);
                    //adapter.log.info("version: " + body.response.version);
                    // see http://www.wunderground.com/weather/api/d/docs?d=resources/phrase-glossary for infos about properties and codes
                    adapter.setState("forecast." + i + "h.time", {ack: true, val: body.hourly_forecast[i].FCTTIME.pretty});
                    adapter.setState("forecast." + i + "h.temp", {ack: true, val: body.hourly_forecast[i].temp.metric});
                    adapter.setState("forecast." + i + "h.fctcode", {ack: true, val: body.hourly_forecast[i].fctcode}); //forecast description number -> see link above
                    adapter.setState("forecast." + i + "h.sky", {ack: true, val: body.hourly_forecast[i].sky}); //?
                    adapter.setState("forecast." + i + "h.wspd", {ack: true, val: body.hourly_forecast[i].wspd.metric}); // windspeed in kmh
                    adapter.setState("forecast." + i + "h.wdir", {ack: true, val: body.hourly_forecast[i].wdir.degrees}); //wind dir in degrees
                    adapter.setState("forecast." + i + "h.uvi", {ack: true, val: body.hourly_forecast[i].uvi}); //UV Index -> wikipedia
                    adapter.setState("forecast." + i + "h.humidity", {ack: true, val: body.hourly_forecast[i].humidity});
                    adapter.setState("forecast." + i + "h.heatindex", {ack: true, val: body.hourly_forecast[i].heatindex.metric}); // -> wikipedia
                    adapter.setState("forecast." + i + "h.feelslike", {ack: true, val: body.hourly_forecast[i].feelslike.metric}); // -> wikipedia
                    adapter.setState("forecast." + i + "h.qpf", {ack: true, val: body.hourly_forecast[i].qpf.metric}); // Quantitative precipitation forecast
                    adapter.setState("forecast." + i + "h.snow", {ack: true, val: body.hourly_forecast[i].snow.metric});
                    adapter.setState("forecast." + i + "h.pop", {ack: true, val: body.hourly_forecast[i].pop}); // probability of Precipitation
                    adapter.setState("forecast." + i + "h.mslp", {ack: true, val: body.hourly_forecast[i].mslp.metric}); // mean sea level pressure

                    qpf_sum += Number(body.hourly_forecast[i].qpf.metric);
                    uvi_sum += Number(body.hourly_forecast[i].uvi);
                    if (Number(body.hourly_forecast[i].uvi) > pop_max) {
                        pop_max = Number(body.hourly_forecast[i].uvi);
                    }

                    // 6h
                    if (i == 5) {
                        adapter.setState("forecast.6h.sum.qpf", {ack: true, val: qpf_sum});
                        adapter.setState("forecast.6h.sum.pop", {ack: true, val: pop_max});
                        adapter.setState("forecast.6h.sum.uvi", {ack: true, val: uvi_sum/6});
                    }
                    // 12h
                    if (i == 11) {
                        adapter.setState("forecast.12h.sum.qpf", {ack: true, val: qpf_sum});
                        adapter.setState("forecast.12h.sum.pop", {ack: true, val: pop_max});
                        adapter.setState("forecast.12h.sum.uvi", {ack: true, val: uvi_sum/12});
                    }
                    // 24h
                    if (i == 23) {
                        adapter.setState("forecast.24h.sum.qpf", {ack: true, val: qpf_sum});
                        adapter.setState("forecast.24h.sum.pop", {ack: true, val: pop_max});
                        adapter.setState("forecast.24h.sum.uvi", {ack: true, val: uvi_sum/24});
                    }
                } catch (error) {
                    adapter.log.error("Could not parse Forecast-Data: " + error);
                }
            }
            adapter.log.info("all forecast values set");
        } else
        {
            // ERROR
            adapter.log.error("Wunderground reported an error: " + error);
        }
    });
}

function checkWeatherVariables() {
    adapter.log.info("init forecast variables", 'info');

    ['time','temp','fctcode','sky','wspd','wdir','uvi','humidity','heatindex','feelslike','qpf','snow','pop','mslp'].map( function(v) {
        for (var i = 0; i < 24; i++) {
            var name = "forecast." + i + "h." + v;
            //if (adapter.getState(name) === null) {
                var obj = {
                    type: 'state',
                    common: {name: v, read: true, write: true, type: 'number'},
                    native: {id: name}
                };
                adapter.setObjectNotExists(name, obj);
                adapter.log.info("created object: " + name);
            //}
        }
    });
    ['forecast.6h.sum.qpf','forecast.12h.sum.qpf','forecast.24h.sum.qpf',
        'forecast.6h.sum.pop', 'forecast.12h.sum.pop', 'forecast.24h.sum.pop',
        'forecast.6h.sum.uvi','forecast.12h.sum.uvi','forecast.24h.sum.uvi'].map( function(name) {
            //if (adapter.getState(name) === null) {
                var obj = {
                    type: 'state',
                    common: {name: name, read: true, write: true, type: 'number'},
                    native: {id: name}
                };
                adapter.setObjectNotExists(name, obj);
            //}
        });
}

<|endoftext|>


<commit_before>import React, {Component} from 'react'
import {connect} from 'react-redux'

import DropDown from 'gComponents/utility/drop-down/index'
import {CardListElement} from 'gComponents/utility/card/index'

import * as meActions from 'gModules/me/actions'
import * as modalActions from 'gModules/modal/actions'
import * as navigationActions from 'gModules/navigation/actions'
import * as spaceActions from 'gModules/spaces/actions'

import {user,organization} from 'gEngine/engine'

import {trackAccountModalClick, trackUserMenuOpen, trackUserMenuClose} from 'servers/segment/index'

import './style.css'

@connect()
export default class Profile extends Component {
  displayName: 'Profile'

  signUp() {
    this.props.dispatch(meActions.signUp())
  }

  signIn() {
    this.props.dispatch(meActions.signIn())
  }

  logOut() {
    this.props.dispatch(meActions.logOut())
  }

  newModel(organizationId) {
    this.props.dispatch(spaceActions.create(organizationId))
  }

  _openModal() {
    trackAccountModalClick()
    this.props.dispatch(modalActions.openSettings())
  }

  profileDropdown() {
    const profile = this.props.me.profile
    const portalUrl = _.get(profile, 'account._links.payment_portal.href')

    let listElements = [
      {ionicIcon: 'md-person', header: 'account', onMouseDown: this._openModal.bind(this)},
      {icon: 'rocket', header: 'upgrade', onMouseDown: () => {navigationActions.navigate('/subscribe/lite')}},
      {ionicIcon: 'ios-people', header: 'New Organization', onMouseDown: () => {navigationActions.navigate('/organizations/new')}},
      {ionicIcon: 'md-help', header: 'Documentation', onMouseDown: () => {navigationActions.externalNavigate('http://docs.getguesstimate.com/')}},
      {ionicIcon: 'md-log-out', header: 'Sign Out', onMouseDown: this.logOut.bind(this)}
    ]

    if (!!portalUrl) {
      listElements = listElements.filter(e => (e.header !== 'upgrade'))
    }

    return (

        <DropDown
          headerText={profile.name}
          openLink={}
          onOpen={trackUserMenuOpen}
          onClose={trackUserMenuClose}
        >
          {listElements.map(props => <CardListElement {...props} key={props.header} closeOnClick={true}/>)}
        </DropDown>

    )
  }

  closeDropdown(dropDown) {
    this.refs[dropDown] && this.refs[dropDown]._close()
  }

  newModelDropdown(me, organizations) {
    const ref = 'newModel'
    const userPic = _.get(me, 'profile.picture')
    const userName = _.get(me, 'profile.name')
    const personalModel = {
      id: 0,
      header: userName,
      imageShape: 'circle',
      onMouseDown: ()=>{this.newModel(); this.closeDropdown(ref)}
    }
    userPic ? (personalModel.image = userPic) : (personalModel.icon = 'user')
    let listElements = [ {props: personalModel}]
    if (organizations) {
      listElements = listElements.concat(organizations.map(
        o => (
          {
            props: {
              header: `${o.name}`,
              imageShape: 'circle',
              image: o.picture,
              onMouseDown: () => {this.newModel(o.id); this.closeDropdown(ref)}
            },
            id: o.id
          }
        )
      ))
    }

    return (
      <DropDown
        headerText={'Select Owner'}
        openLink={ <i className={`ion-md-add`}/> New Model }
        ref={ref}
      >
        {listElements.map(element => <CardListElement {...element.props} key={element.id}/>)}
      </DropDown>
    )
  }

  organizationsDropdown(organizations) {
    const ref='organizations'
    let listElements = []

    if (organizations) {
      listElements = organizations.map(
        o => (
          {
            props: {
              header: `${o.name}`,
              imageShape: 'circle',
              image: organization.image(o),
              onMouseDown: () => {navigationActions.navigate(organization.url(o)); this.closeDropdown(ref)}
            },
            id: o.id
          }
        )
      )
    }

    return (
      <DropDown
        headerText={'Organizations'}
        openLink={ <i className={`ion-ios-people`}/> Organizations }
        ref={ref}
      >
        {listElements.map(element => <CardListElement {...element.props} key={element.id} />)}
      </DropDown>
    )
  }

  render () {
    const {me, isLoggedIn} = this.props
    const organizations = user.usersOrganizations(this.props.me,
                                                  this.props.userOrganizationMemberships,
                                                  this.props.organizations)
    const hasOrganizations = organizations.length > 0

    return (

      { isLoggedIn && hasOrganizations && this.newModelDropdown(me, organizations) }
      { isLoggedIn && !hasOrganizations &&

          <i className={`ion-md-add`}/>
          New Model

      }

      { isLoggedIn && me.id &&
        <a className='item' href={`/users/${me.id}`}>
          <i className={`ion-md-albums`}/>
          My Models

      }

      { isLoggedIn && hasOrganizations && this.organizationsDropdown(organizations) }

      { isLoggedIn && this.profileDropdown() }

      { !isLoggedIn &&
        Scratchpad
      }

      { !isLoggedIn &&
        Plans
      }

      { !isLoggedIn &&
        Sign In
      }

      { !isLoggedIn &&
        Sign Up
      }

    )
  }
}
<commit_msg>Add link for product feedback<commit_after>import React, {Component} from 'react'
import {connect} from 'react-redux'

import DropDown from 'gComponents/utility/drop-down/index'
import {CardListElement} from 'gComponents/utility/card/index'

import * as meActions from 'gModules/me/actions'
import * as modalActions from 'gModules/modal/actions'
import * as navigationActions from 'gModules/navigation/actions'
import * as spaceActions from 'gModules/spaces/actions'

import {user,organization} from 'gEngine/engine'

import {trackAccountModalClick, trackUserMenuOpen, trackUserMenuClose} from 'servers/segment/index'

import './style.css'

@connect()
export default class Profile extends Component {
  displayName: 'Profile'

  signUp() {
    this.props.dispatch(meActions.signUp())
  }

  signIn() {
    this.props.dispatch(meActions.signIn())
  }

  logOut() {
    this.props.dispatch(meActions.logOut())
  }

  newModel(organizationId) {
    this.props.dispatch(spaceActions.create(organizationId))
  }

  _openModal() {
    trackAccountModalClick()
    this.props.dispatch(modalActions.openSettings())
  }

  profileDropdown() {
    const profile = this.props.me.profile
    const portalUrl = _.get(profile, 'account._links.payment_portal.href')

    let listElements = [
      {ionicIcon: 'md-person', header: 'account', onMouseDown: this._openModal.bind(this)},
      {icon: 'rocket', header: 'upgrade', onMouseDown: () => {navigationActions.navigate('/subscribe/lite')}},
      {ionicIcon: 'ios-people', header: 'New Organization', onMouseDown: () => {navigationActions.navigate('/organizations/new')}},
      {ionicIcon: 'md-help', header: 'Documentation', onMouseDown: () => {navigationActions.externalNavigate('http://docs.getguesstimate.com/')}},
      {ionicIcon: 'ios-chatbubbles', header: 'Feedback', onMouseDown: () => {navigationActions.externalNavigate('https://productpains.com/product/guesstimate')}},
      {ionicIcon: 'md-log-out', header: 'Sign Out', onMouseDown: this.logOut.bind(this)}
    ]

    if (!!portalUrl) {
      listElements = listElements.filter(e => (e.header !== 'upgrade'))
    }

    return (

        <DropDown
          headerText={profile.name}
          openLink={}
          onOpen={trackUserMenuOpen}
          onClose={trackUserMenuClose}
        >
          {listElements.map(props => <CardListElement {...props} key={props.header} closeOnClick={true}/>)}
        </DropDown>

    )
  }

  closeDropdown(dropDown) {
    this.refs[dropDown] && this.refs[dropDown]._close()
  }

  newModelDropdown(me, organizations) {
    const ref = 'newModel'
    const userPic = _.get(me, 'profile.picture')
    const userName = _.get(me, 'profile.name')
    const personalModel = {
      id: 0,
      header: userName,
      imageShape: 'circle',
      onMouseDown: ()=>{this.newModel(); this.closeDropdown(ref)}
    }
    userPic ? (personalModel.image = userPic) : (personalModel.icon = 'user')
    let listElements = [ {props: personalModel}]
    if (organizations) {
      listElements = listElements.concat(organizations.map(
        o => (
          {
            props: {
              header: `${o.name}`,
              imageShape: 'circle',
              image: o.picture,
              onMouseDown: () => {this.newModel(o.id); this.closeDropdown(ref)}
            },
            id: o.id
          }
        )
      ))
    }

    return (
      <DropDown
        headerText={'Select Owner'}
        openLink={ <i className={`ion-md-add`}/> New Model }
        ref={ref}
      >
        {listElements.map(element => <CardListElement {...element.props} key={element.id}/>)}
      </DropDown>
    )
  }

  organizationsDropdown(organizations) {
    const ref='organizations'
    let listElements = []

    if (organizations) {
      listElements = organizations.map(
        o => (
          {
            props: {
              header: `${o.name}`,
              imageShape: 'circle',
              image: organization.image(o),
              onMouseDown: () => {navigationActions.navigate(organization.url(o)); this.closeDropdown(ref)}
            },
            id: o.id
          }
        )
      )
    }

    return (
      <DropDown
        headerText={'Organizations'}
        openLink={ <i className={`ion-ios-people`}/> Organizations }
        ref={ref}
      >
        {listElements.map(element => <CardListElement {...element.props} key={element.id} />)}
      </DropDown>
    )
  }

  render () {
    const {me, isLoggedIn} = this.props
    const organizations = user.usersOrganizations(this.props.me,
                                                  this.props.userOrganizationMemberships,
                                                  this.props.organizations)
    const hasOrganizations = organizations.length > 0

    return (

      { isLoggedIn && hasOrganizations && this.newModelDropdown(me, organizations) }
      { isLoggedIn && !hasOrganizations &&

          <i className={`ion-md-add`}/>
          New Model

      }

      { isLoggedIn && me.id &&
        <a className='item' href={`/users/${me.id}`}>
          <i className={`ion-md-albums`}/>
          My Models

      }

      { isLoggedIn && hasOrganizations && this.organizationsDropdown(organizations) }

      { isLoggedIn && this.profileDropdown() }

      { !isLoggedIn &&
        Scratchpad
      }

      { !isLoggedIn &&
        Plans
      }

      { !isLoggedIn &&
        Sign In
      }

      { !isLoggedIn &&
        Sign Up
      }

    )
  }
}
<|endoftext|>


<commit_before>/**
 * Copyright 2019 The Subscribe with Google Authors. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS-IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import {AnalyticsEvent, EntitlementResult} from '../proto/api_messages';
import {Event} from '../api/logger-api';
import {PublisherEntitlementEvent} from '../api/subscriptions';

/** @const {!Object<string,AnalyticsEvent>} */
const PublisherEventToAnalyticsEvent = {
  [Event.IMPRESSION_PAYWALL]: AnalyticsEvent.IMPRESSION_PAYWALL,
  [Event.IMPRESSION_AD]: AnalyticsEvent.IMPRESSION_AD,
  [Event.IMPRESSION_OFFERS]: AnalyticsEvent.IMPRESSION_OFFERS,
  [Event.ACTION_SUBSCRIPTIONS_LANDING_PAGE]:
    AnalyticsEvent.ACTION_SUBSCRIPTIONS_LANDING_PAGE,
  [Event.ACTION_OFFER_SELECTED]: AnalyticsEvent.ACTION_OFFER_SELECTED,
  [Event.ACTION_PAYMENT_FLOW_STARTED]:
    AnalyticsEvent.ACTION_PAYMENT_FLOW_STARTED,
  [Event.ACTION_PAYMENT_COMPLETED]: AnalyticsEvent.ACTION_PAYMENT_COMPLETE,
  [Event.EVENT_CUSTOM]: AnalyticsEvent.EVENT_CUSTOM,
};

/** @const {!Object<?AnalyticsEvent,?Event>} */
const AnalyticsEventToPublisherEvent = {
  [AnalyticsEvent.UNKNOWN]: null,
  [AnalyticsEvent.IMPRESSION_PAYWALL]: Event.IMPRESSION_PAYWALL,
  [AnalyticsEvent.IMPRESSION_AD]: Event.IMPRESSION_AD,
  [AnalyticsEvent.IMPRESSION_OFFERS]: Event.IMPRESSION_OFFERS,
  [AnalyticsEvent.IMPRESSION_SUBSCRIBE_BUTTON]: null,
  [AnalyticsEvent.IMPRESSION_SMARTBOX]: null,
  [AnalyticsEvent.ACTION_SUBSCRIBE]: null,
  [AnalyticsEvent.ACTION_PAYMENT_COMPLETE]: Event.ACTION_PAYMENT_COMPLETED,
  [AnalyticsEvent.ACTION_ACCOUNT_CREATED]: null,
  [AnalyticsEvent.ACTION_ACCOUNT_ACKNOWLEDGED]: null,
  [AnalyticsEvent.ACTION_SUBSCRIPTIONS_LANDING_PAGE]:
    Event.ACTION_SUBSCRIPTIONS_LANDING_PAGE,
  [AnalyticsEvent.ACTION_PAYMENT_FLOW_STARTED]:
    Event.ACTION_PAYMENT_FLOW_STARTED,
  [AnalyticsEvent.ACTION_OFFER_SELECTED]: Event.ACTION_OFFER_SELECTED,
  [AnalyticsEvent.EVENT_PAYMENT_FAILED]: null,
  [AnalyticsEvent.EVENT_CUSTOM]: Event.EVENT_CUSTOM,
};

/** @const {!Object<string,?Array<AnalyticsEvent>>} */
const ShowcaseEntitlemenntToAnalyticsEvents = {
  [PublisherEntitlementEvent.EVENT_SHOWCASE_UNLOCKED_BY_SUBSCRIPTION]: [
    AnalyticsEvent.EVENT_UNLOCKED_BY_SUBSCRIPTION,
  ],
  [PublisherEntitlementEvent.EVENT_SHOWCASE_UNLOCKED_BY_METER]: [
    AnalyticsEvent.EVENT_HAS_METERING_ENTITLEMENTS,
    AnalyticsEvent.EVENT_UNLOCKED_BY_METER,
  ],
  [PublisherEntitlementEvent.EVENT_SHOWCASE_UNLOCKED_FREE_PAGE]: [
    AnalyticsEvent.EVENT_UNLOCKED_FREE_PAGE,
  ],
  [PublisherEntitlementEvent.EVENT_SHOWCASE_NO_ENTITLEMENTS_REGWALL]: [
    AnalyticsEvent.EVENT_NO_ENTITLEMENTS,
    AnalyticsEvent.IMPRESSION_REGWALL,
    AnalyticsEvent.IMPRESSION_SHOWCASE_REGWALL,
  ],
  [PublisherEntitlementEvent.EVENT_SHOWCASE_NO_ENTITLEMENTS_PAYWALL]: [
    AnalyticsEvent.EVENT_NO_ENTITLEMENTS,
    AnalyticsEvent.IMPRESSION_PAYWALL,
  ],
  [PublisherEntitlementEvent.EVENT_SHOWCASE_METER_OFFERED]: [
    AnalyticsEvent.EVENT_HAS_METERING_ENTITLEMENTS,
    AnalyticsEvent.EVENT_OFFERED_METER,
  ],
};

/** @const {!Object<?AnalyticsEvent,?Event>} */
const AnalyticsEventToEntitlementResult = {
  [AnalyticsEvent.IMPRESSION_REGWALL]: EntitlementResult.LOCKED_REGWALL,
  [AnalyticsEvent.EVENT_UNLOCKED_BY_METER]: EntitlementResult.UNLOCKED_METER,
  [AnalyticsEvent.EVENT_UNLOCKED_BY_SUBSCRIPTION]:
    EntitlementResult.UNLOCKED_SUBSCRIBER,
  [AnalyticsEvent.EVENT_UNLOCKED_FREE_PAGE]: EntitlementResult.UNLOCKED_FREE,
  [AnalyticsEvent.IMPRESSION_PAYWALL]: EntitlementResult.LOCKED_PAYWALL,
};

/**
 * Converts a propensity event enum into an analytics event enum.
 * @param {!Event|string} propensityEvent
 * @returns {!AnalyticsEvent}
 */
export function publisherEventToAnalyticsEvent(propensityEvent) {
  return PublisherEventToAnalyticsEvent[propensityEvent];
}

/**
 * Converts an analytics event enum into a propensity event enum.
 * @param {?AnalyticsEvent} analyticsEvent
 * @returns {?Event}
 */
export function analyticsEventToPublisherEvent(analyticsEvent) {
  return AnalyticsEventToPublisherEvent[analyticsEvent];
}

/**
 * Converts a publisher entitlement event enum into an array analytics events.
 * @param {!PublisherEntitlementEvent|string} event
 * @returns {!Array<AnalyticsEvent>}
 */
export function publisherEntitlementEventToAnalyticsEvents(event) {
  return ShowcaseEntitlemenntToAnalyticsEvents[event] || [];
}

export function analyticsEventToEntitlementResult(event) {
  return AnalyticsEventToEntitlementResult[event];
}
<commit_msg>Initial implementation<commit_after>/**
 * Copyright 2019 The Subscribe with Google Authors. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS-IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import {AnalyticsEvent, EntitlementResult} from '../proto/api_messages';
import {Event} from '../api/logger-api';
import {PublisherEntitlementEvent} from '../api/subscriptions';

/** @const {!Object<string,AnalyticsEvent>} */
const PublisherEventToAnalyticsEvent = {
  [Event.IMPRESSION_PAYWALL]: AnalyticsEvent.IMPRESSION_PAYWALL,
  [Event.IMPRESSION_AD]: AnalyticsEvent.IMPRESSION_AD,
  [Event.IMPRESSION_OFFERS]: AnalyticsEvent.IMPRESSION_OFFERS,
  [Event.ACTION_SUBSCRIPTIONS_LANDING_PAGE]:
    AnalyticsEvent.ACTION_SUBSCRIPTIONS_LANDING_PAGE,
  [Event.ACTION_OFFER_SELECTED]: AnalyticsEvent.ACTION_OFFER_SELECTED,
  [Event.ACTION_PAYMENT_FLOW_STARTED]:
    AnalyticsEvent.ACTION_PAYMENT_FLOW_STARTED,
  [Event.ACTION_PAYMENT_COMPLETED]: AnalyticsEvent.ACTION_PAYMENT_COMPLETE,
  [Event.EVENT_CUSTOM]: AnalyticsEvent.EVENT_CUSTOM,
};

/** @const {!Object<?AnalyticsEvent,?Event>} */
const AnalyticsEventToPublisherEvent = {
  [AnalyticsEvent.UNKNOWN]: null,
  [AnalyticsEvent.IMPRESSION_PAYWALL]: Event.IMPRESSION_PAYWALL,
  [AnalyticsEvent.IMPRESSION_AD]: Event.IMPRESSION_AD,
  [AnalyticsEvent.IMPRESSION_OFFERS]: Event.IMPRESSION_OFFERS,
  [AnalyticsEvent.IMPRESSION_SUBSCRIBE_BUTTON]: null,
  [AnalyticsEvent.IMPRESSION_SMARTBOX]: null,
  [AnalyticsEvent.ACTION_SUBSCRIBE]: null,
  [AnalyticsEvent.ACTION_PAYMENT_COMPLETE]: Event.ACTION_PAYMENT_COMPLETED,
  [AnalyticsEvent.ACTION_ACCOUNT_CREATED]: null,
  [AnalyticsEvent.ACTION_ACCOUNT_ACKNOWLEDGED]: null,
  [AnalyticsEvent.ACTION_SUBSCRIPTIONS_LANDING_PAGE]:
    Event.ACTION_SUBSCRIPTIONS_LANDING_PAGE,
  [AnalyticsEvent.ACTION_PAYMENT_FLOW_STARTED]:
    Event.ACTION_PAYMENT_FLOW_STARTED,
  [AnalyticsEvent.ACTION_OFFER_SELECTED]: Event.ACTION_OFFER_SELECTED,
  [AnalyticsEvent.EVENT_PAYMENT_FAILED]: null,
  [AnalyticsEvent.EVENT_CUSTOM]: Event.EVENT_CUSTOM,
};

/** @const {!Object<string,?Array<AnalyticsEvent>>} */
const ShowcaseEntitlemenntToAnalyticsEvents = {
  [PublisherEntitlementEvent.EVENT_SHOWCASE_UNLOCKED_BY_SUBSCRIPTION]: [
    AnalyticsEvent.EVENT_UNLOCKED_BY_SUBSCRIPTION,
  ],
  [PublisherEntitlementEvent.EVENT_SHOWCASE_UNLOCKED_BY_METER]: [
    AnalyticsEvent.EVENT_HAS_METERING_ENTITLEMENTS,
    AnalyticsEvent.EVENT_UNLOCKED_BY_METER,
  ],
  [PublisherEntitlementEvent.EVENT_SHOWCASE_UNLOCKED_FREE_PAGE]: [
    AnalyticsEvent.EVENT_UNLOCKED_FREE_PAGE,
  ],
  [PublisherEntitlementEvent.EVENT_SHOWCASE_NO_ENTITLEMENTS_REGWALL]: [
    AnalyticsEvent.EVENT_NO_ENTITLEMENTS,
    AnalyticsEvent.IMPRESSION_REGWALL,
    AnalyticsEvent.IMPRESSION_SHOWCASE_REGWALL,
  ],
  [PublisherEntitlementEvent.EVENT_SHOWCASE_NO_ENTITLEMENTS_PAYWALL]: [
    AnalyticsEvent.EVENT_NO_ENTITLEMENTS,
    AnalyticsEvent.IMPRESSION_PAYWALL,
  ],
  [PublisherEntitlementEvent.EVENT_SHOWCASE_METER_OFFERED]: [
    AnalyticsEvent.EVENT_HAS_METERING_ENTITLEMENTS,
    AnalyticsEvent.EVENT_OFFERED_METER,
  ],
};

/** @const {!Object<?AnalyticsEvent,?Event>} */
const AnalyticsEventToEntitlementResult = {
  [AnalyticsEvent.IMPRESSION_REGWALL]: EntitlementResult.LOCKED_REGWALL,
  [AnalyticsEvent.EVENT_UNLOCKED_BY_METER]: EntitlementResult.UNLOCKED_METER,
  [AnalyticsEvent.EVENT_UNLOCKED_BY_SUBSCRIPTION]:
    EntitlementResult.UNLOCKED_SUBSCRIBER,
  [AnalyticsEvent.EVENT_UNLOCKED_FREE_PAGE]: EntitlementResult.UNLOCKED_FREE,
  [AnalyticsEvent.IMPRESSION_PAYWALL]: EntitlementResult.LOCKED_PAYWALL,
};

/**
 * @param {!string} eventCategory
 * @param {!string} eventAction
 * @param {!string} eventLabel
 * @param {!boolean} nonInteraction
 * @returns {!Object}
 */
const createGoogleAnalyticsEvent = (
  eventCategory,
  eventAction,
  eventLabel,
  nonInteraction
) => ({
  eventCategory,
  eventAction,
  eventLabel,
  nonInteraction,
});

/** @const {!Object<?AnalyticsEvent,?Object>} */
const AnalyticsEventToGoogleAnalyticsEvent = {
  [AnalyticsEvent.IMPRESSION_OFFERS]: createGoogleAnalyticsEvent(
    'NTG paywall',
    'paywall modal impression',
    '',
    true
  ),
  [AnalyticsEvent.ACTION_OFFER_SELECTED]: createGoogleAnalyticsEvent(
    'NTG paywall',
    'click',
    '',
    false
  ),

  [AnalyticsEvent.ACTION_SWG_SUBSCRIPTION_MINI_PROMPT_CLICK]: createGoogleAnalyticsEvent(
    'NTG subscription',
    'marketing modal click',
    '',
    false
  ),
  [AnalyticsEvent.IMPRESSION_SWG_SUBSCRIPTION_MINI_PROMPT]: createGoogleAnalyticsEvent(
    'NTG subscription',
    'marketing modal impression',
    '',
    true
  ),
  [AnalyticsEvent.ACTION_SWG_CONTRIBUTION_MINI_PROMPT_CLICK]: createGoogleAnalyticsEvent(
    'NTG membership',
    'marketing modal click',
    '',
    false
  ),
  [AnalyticsEvent.IMPRESSION_SWG_CONTRIBUTION_MINI_PROMPT]: createGoogleAnalyticsEvent(
    'NTG membership',
    'membership modal impression',
    '',
    true
  ),
};

/**
 * Converts a propensity event enum into an analytics event enum.
 * @param {!Event|string} propensityEvent
 * @returns {!AnalyticsEvent}
 */
export function publisherEventToAnalyticsEvent(propensityEvent) {
  return PublisherEventToAnalyticsEvent[propensityEvent];
}

/**
 * Converts an analytics event enum into a propensity event enum.
 * @param {?AnalyticsEvent} analyticsEvent
 * @returns {?Event}
 */
export function analyticsEventToPublisherEvent(analyticsEvent) {
  return AnalyticsEventToPublisherEvent[analyticsEvent];
}

/**
 * Converts a publisher entitlement event enum into an array analytics events.
 * @param {!PublisherEntitlementEvent|string} event
 * @returns {!Array<AnalyticsEvent>}
 */
export function publisherEntitlementEventToAnalyticsEvents(event) {
  return ShowcaseEntitlemenntToAnalyticsEvents[event] || [];
}

export function analyticsEventToEntitlementResult(event) {
  return AnalyticsEventToEntitlementResult[event];
}

/**
 * Converts an analytics event enum into a Google Analytics event object.
 * @param {?AnalyticsEvent} analyticsEvent
 * @returns {?Object}
 */
export function analyticsEventToGoogleAnalyticsEvent(event) {
  return AnalyticsEventToGoogleAnalyticsEvent[event];
}
<|endoftext|>


<commit_before>import { string, basicFunction } from 'source/common/verify.js'
import { isString, isBasicFunction } from 'source/common/check.js'
import { catchSync, catchAsync } from 'source/common/error.js'
import { clock } from 'source/common/time.js'
import { time } from 'source/common/format.js'
import { indentLine } from 'source/common/string.js'
import { withTimeoutAsync } from 'source/common/function.js'
import { createTreeDepthFirstSearchAsync } from 'source/common/data/Tree.js'

const LIST_NAME_BEFORE = 'beforeList'
const LIST_NAME_IT = 'itList'
const LIST_NAME_AFTER = 'afterList'

const createScope = (
  upperScope = null,
  level = 0,
  title = ''
) => ({
  upperScope, level, title,
  [ LIST_NAME_BEFORE ]: [],
  [ LIST_NAME_IT ]: [],
  [ LIST_NAME_AFTER ]: []
})

const walkScopeAsync = createTreeDepthFirstSearchAsync((scope) => scope.func ? [] : [
  ...scope[ LIST_NAME_BEFORE ],
  ...scope[ LIST_NAME_IT ],
  ...scope[ LIST_NAME_AFTER ]
])

const getTitleStack = (title, upperScope) => {
  const titleStack = [ title ]
  while (upperScope) {
    titleStack.unshift(upperScope.title)
    upperScope = upperScope.upperScope
  }
  return titleStack
}

const createTest = ({ // BDD style
  colorFallback = (text) => text,
  colorError = colorFallback,
  colorMain = colorFallback,
  colorTitle = colorFallback,
  colorText = colorFallback,
  colorTime = colorFallback
} = {}) => {
  let ROOT_SCOPE
  let CURRENT_SCOPE
  let RESULT
  let CONFIG

  const describe = (title, setupFunc) => {
    string(title, 'invalid describe title')
    basicFunction(setupFunc, 'invalid describe setupFunc')
    if (CURRENT_SCOPE === undefined) throw new Error(`should add "describe()" between "TEST_SETUP()" & "TEST_RUN()": ${title}`)
    const innerScope = createScope(CURRENT_SCOPE, CURRENT_SCOPE.level + 1, title)
    CURRENT_SCOPE[ LIST_NAME_IT ].push(innerScope)
    CURRENT_SCOPE = innerScope
    const { result, error } = catchSync(setupFunc) // TODO: should be sync
    if (result instanceof Promise) throw new Error(`unexpect Promise from describe: ${title}`)
    if (error) {
      CONFIG.log(colorError(`error from describe: ${title}`))
      throw error
    }
    CURRENT_SCOPE = CURRENT_SCOPE.upperScope
  }

  const getPushScopeFunc = (type, typeListName) => (title, func) => {
    if (func === undefined && isBasicFunction(title)) [ title, func ] = [ func, title ] // swap to support auto naming, but this is not a good test habit
    if (!isString(title)) title = `${CURRENT_SCOPE.title} - ${type} #${CURRENT_SCOPE[ typeListName ].length}`
    string(title, `invalid ${type} title`)
    basicFunction(func, `invalid ${type} func`)
    if (CURRENT_SCOPE === undefined) throw new Error(`should "${type}()" between "TEST_SETUP()" & "TEST_RUN()": ${title}`)
    CURRENT_SCOPE[ typeListName ].push({
      upperScope: CURRENT_SCOPE,
      level: CURRENT_SCOPE.level + 1,
      title,
      func
    })
  }

  const before = getPushScopeFunc('before', LIST_NAME_BEFORE)
  const it = getPushScopeFunc('it', LIST_NAME_IT)
  const after = getPushScopeFunc('after', LIST_NAME_AFTER)

  const info = (...args) => CONFIG && CONFIG.log(indentLine(args.join(' '), '      > '))

  const TEST_SETUP = ({
    log = console.log,
    logLevel = (level, ...args) => log(indentLine(args.join(' '), '  '.repeat(level))),
    timeout = 42 * 1000, // 42 sec, for each test
    testGlobal = globalThis,
    isSkipGlobalAssign = false
  } = {}) => {
    ROOT_SCOPE = CURRENT_SCOPE = createScope(null, 0, 'root')
    RESULT = { passList: [], failList: [] }
    CONFIG = { log, logLevel, timeout }

    // inject global for test script
    !isSkipGlobalAssign && Object.assign(testGlobal, { describe, it, before, after, info })

    CONFIG.log(colorMain('[TEST] setup'))
  }

  const TEST_RUN = async () => {
    CONFIG.log(colorMain('[TEST] run'))

    // reset
    const scope = ROOT_SCOPE
    ROOT_SCOPE = CURRENT_SCOPE = undefined

    const runStart = clock()

    await walkScopeAsync(scope, async ({
      upperScope,
      title,
      level,

      func
    }) => {
      if (!func) { // describe
        CONFIG.logLevel(level, colorTitle(title))
      } else {
        const funcStart = clock()
        const { error } = await catchAsync(withTimeoutAsync, func, CONFIG.timeout)
        const funcDuration = clock() - funcStart
        const timeLog = funcDuration > 64 ? colorTime(`(${time(funcDuration)})`) : ''
        if (error) {
          RESULT.failList.push({ titleStack: getTitleStack(title, upperScope), error })
          CONFIG.logLevel(level, colorError(`[FAIL] ${title}`), timeLog)
          CONFIG.logLevel(level, colorError(indentLine(error.stack || error, '    ')))
        } else {
          RESULT.passList.push({ title })
          CONFIG.logLevel(level, colorText(title), timeLog)
        }
      }
    })

    CONFIG.log(colorMain([
      `[TEST] done in ${time(clock() - runStart)}`,
      `  pass: ${RESULT.passList.length}`,
      `  fail: ${RESULT.failList.length}`
    ].join('\n')))

    for (let index = 0, indexMax = RESULT.failList.length; index < indexMax; index++) {
      const { titleStack, error } = RESULT.failList[ index ]
      CONFIG.log(colorError(`[FAIL|${index}]`))
      titleStack.forEach((title, level) => level && CONFIG.logLevel(level, colorError(title))) // skip root level
      CONFIG.logLevel(titleStack.length, colorError(indentLine(error.stack || error, '    ')))
    }

    return RESULT
  }

  return {
    TEST_SETUP, TEST_RUN,
    describe, it, before, after, info
  }
}

export { createTest }
<commit_msg>CHG: test: better `info` output with `util.inspect`<commit_after>import { tryRequire } from 'source/env/tryRequire.js'
import { string, basicFunction } from 'source/common/verify.js'
import { isString, isBasicFunction } from 'source/common/check.js'
import { catchSync, catchAsync } from 'source/common/error.js'
import { clock } from 'source/common/time.js'
import { time } from 'source/common/format.js'
import { indentLine } from 'source/common/string.js'
import { withTimeoutAsync } from 'source/common/function.js'
import { createTreeDepthFirstSearchAsync } from 'source/common/data/Tree.js'

const LIST_NAME_BEFORE = 'beforeList'
const LIST_NAME_IT = 'itList'
const LIST_NAME_AFTER = 'afterList'

const createScope = (
  upperScope = null,
  level = 0,
  title = ''
) => ({
  upperScope, level, title,
  [ LIST_NAME_BEFORE ]: [],
  [ LIST_NAME_IT ]: [],
  [ LIST_NAME_AFTER ]: []
})

const walkScopeAsync = createTreeDepthFirstSearchAsync((scope) => scope.func ? [] : [
  ...scope[ LIST_NAME_BEFORE ],
  ...scope[ LIST_NAME_IT ],
  ...scope[ LIST_NAME_AFTER ]
])

const getTitleStack = (title, upperScope) => {
  const titleStack = [ title ]
  while (upperScope) {
    titleStack.unshift(upperScope.title)
    upperScope = upperScope.upperScope
  }
  return titleStack
}

const createTest = ({ // BDD style
  colorFallback = (text) => text,
  colorError = colorFallback,
  colorMain = colorFallback,
  colorTitle = colorFallback,
  colorText = colorFallback,
  colorTime = colorFallback
} = {}) => {
  let ROOT_SCOPE
  let CURRENT_SCOPE
  let RESULT
  let CONFIG

  const describe = (title, setupFunc) => {
    string(title, 'invalid describe title')
    basicFunction(setupFunc, 'invalid describe setupFunc')
    if (CURRENT_SCOPE === undefined) throw new Error(`should add "describe()" between "TEST_SETUP()" & "TEST_RUN()": ${title}`)
    const innerScope = createScope(CURRENT_SCOPE, CURRENT_SCOPE.level + 1, title)
    CURRENT_SCOPE[ LIST_NAME_IT ].push(innerScope)
    CURRENT_SCOPE = innerScope
    const { result, error } = catchSync(setupFunc) // TODO: should be sync
    if (result instanceof Promise) throw new Error(`unexpect Promise from describe: ${title}`)
    if (error) {
      CONFIG.log(colorError(`error from describe: ${title}`))
      throw error
    }
    CURRENT_SCOPE = CURRENT_SCOPE.upperScope
  }

  const getPushScopeFunc = (type, typeListName) => (title, func) => {
    if (func === undefined && isBasicFunction(title)) [ title, func ] = [ func, title ] // swap to support auto naming, but this is not a good test habit
    if (!isString(title)) title = `${CURRENT_SCOPE.title} - ${type} #${CURRENT_SCOPE[ typeListName ].length}`
    string(title, `invalid ${type} title`)
    basicFunction(func, `invalid ${type} func`)
    if (CURRENT_SCOPE === undefined) throw new Error(`should "${type}()" between "TEST_SETUP()" & "TEST_RUN()": ${title}`)
    CURRENT_SCOPE[ typeListName ].push({
      upperScope: CURRENT_SCOPE,
      level: CURRENT_SCOPE.level + 1,
      title,
      func
    })
  }

  const before = getPushScopeFunc('before', LIST_NAME_BEFORE)
  const it = getPushScopeFunc('it', LIST_NAME_IT)
  const after = getPushScopeFunc('after', LIST_NAME_AFTER)

  const stringify = (tryRequire('util') || {}).inspect || String
  const info = (...args) => CONFIG && CONFIG.log(indentLine(args.map((v) => stringify(v)).join(' '), '      > '))

  const TEST_SETUP = ({
    log = console.log,
    logLevel = (level, ...args) => log(indentLine(args.join(' '), '  '.repeat(level))),
    timeout = 42 * 1000, // 42 sec, for each test
    testGlobal = globalThis,
    isSkipGlobalAssign = false
  } = {}) => {
    ROOT_SCOPE = CURRENT_SCOPE = createScope(null, 0, 'root')
    RESULT = { passList: [], failList: [] }
    CONFIG = { log, logLevel, timeout }

    // inject global for test script
    !isSkipGlobalAssign && Object.assign(testGlobal, { describe, it, before, after, info })

    CONFIG.log(colorMain('[TEST] setup'))
  }

  const TEST_RUN = async () => {
    CONFIG.log(colorMain('[TEST] run'))

    // reset
    const scope = ROOT_SCOPE
    ROOT_SCOPE = CURRENT_SCOPE = undefined

    const runStart = clock()

    await walkScopeAsync(scope, async ({
      upperScope,
      title,
      level,

      func
    }) => {
      if (!func) { // describe
        CONFIG.logLevel(level, colorTitle(title))
      } else {
        const funcStart = clock()
        const { error } = await catchAsync(withTimeoutAsync, func, CONFIG.timeout)
        const funcDuration = clock() - funcStart
        const timeLog = funcDuration > 64 ? colorTime(`(${time(funcDuration)})`) : ''
        if (error) {
          RESULT.failList.push({ titleStack: getTitleStack(title, upperScope), error })
          CONFIG.logLevel(level, colorError(`[FAIL] ${title}`), timeLog)
          CONFIG.logLevel(level, colorError(indentLine(error.stack || error, '    ')))
        } else {
          RESULT.passList.push({ title })
          CONFIG.logLevel(level, colorText(title), timeLog)
        }
      }
    })

    CONFIG.log(colorMain([
      `[TEST] done in ${time(clock() - runStart)}`,
      `  pass: ${RESULT.passList.length}`,
      `  fail: ${RESULT.failList.length}`
    ].join('\n')))

    for (let index = 0, indexMax = RESULT.failList.length; index < indexMax; index++) {
      const { titleStack, error } = RESULT.failList[ index ]
      CONFIG.log(colorError(`[FAIL|${index}]`))
      titleStack.forEach((title, level) => level && CONFIG.logLevel(level, colorError(title))) // skip root level
      CONFIG.logLevel(titleStack.length, colorError(indentLine(error.stack || error, '    ')))
    }

    return RESULT
  }

  return {
    TEST_SETUP, TEST_RUN,
    describe, it, before, after, info
  }
}

export { createTest }
<|endoftext|>


<commit_before>/* ------------------------------------------------------------------
* node-gotapi - gotapi-interface-4.js
*
* Copyright (c) 2017, Futomi Hatano, All rights reserved.
* Released under the MIT license
* Date: 2017-01-02
* ---------------------------------------------------------------- */
'use strict';
let mCrypto = require('crypto');

/* ------------------------------------------------------------------
* Constructor: GotapiInterface4(config, returnCallback)
* ---------------------------------------------------------------- */
let GotapiInterface4 = function(config, returnCallback) {
	this.config = config;
	this.returnCallback = returnCallback;
	this.GotapiPluginLoader = require('./gotapi-plugin-loader.js');
	this.plugins = {};
	this.requests = {};
	this.requests_approving = {};
	this.approved_clients = {};
	this.plugin_response_timeout_watch_timer_id = 0;

	this.oncommunication = null;
};

/* ------------------------------------------------------------------
* Method: start()
* ---------------------------------------------------------------- */
GotapiInterface4.prototype.start = function() {
	var promise = new Promise((resolve, reject) => {
		// Find and load the GotAPI Plug-ins
		let plugin_loader = new this.GotapiPluginLoader(this.config);
		plugin_loader.load((plugins) => {
			this.plugins = plugins;
			let list = [];
			for(let path in plugins) {
				let p = plugins[path];
				list.push(p);
				// Set a listener for `message` event fired on the message channel for the Plug-In
				p.channel.on('message', this._receiveMessageFromPlugin.bind(this));
				// Start to watch the timeout of the response from the Plug-In
				this.plugin_response_timeout_watch_timer_id = setInterval(() => {
					this._startWatchPluginResponseTimeout();
				}, 1000);
			}
			resolve(list);
		});
	});
	return promise;
};

GotapiInterface4.prototype._startWatchPluginResponseTimeout = function() {
	let now = Date.now();
	for(let request_code in this.requests) {
		let data = this.requests[request_code];
		let rtime = data['_plugin_req_time'];
		if(now - rtime > this.config.plugin_response_timeout * 1000) {
			data['result'] = 408;
			data['errorCode'] = 408;
			data['errorMessage'] = 'The Plug-In did not respond.';
			this._returnResponseToGotapiServer(data);
			delete this.requests[request_code];
		}
	}
};

/* ------------------------------------------------------------------
* Method: postMessage(data)
* This method is called by the GotAPI Server in order to send a
* message to the Plug-In.
* ---------------------------------------------------------------- */
GotapiInterface4.prototype.postMessage = function(data) {
	let request_code = this._createNewRequestCode();
	data['requestCode'] = request_code;
	data['_plugin_req_time'] = Date.now();
	let prof = data['profile'];
	let attr = data['attribute'];
	if(prof === 'servicediscovery') {
		data['profile'] = 'networkServiceDiscovery';
		data['attribute'] = 'getNetworkServices';
		this.requests[request_code] = data;
		this._requestServiceDiscovery(request_code);
	} else if(prof) {
		let client_id = data['_client']['client_id'];
		let service_id = data['params']['serviceId'];
		if(this.approved_clients[client_id] && this.approved_clients[client_id][service_id]) {
			data['accessToken'] = this.approved_clients[client_id][service_id];
			this.requests[request_code] = data;
			this._requestCommandToPlugin(request_code);
		} else {
			this.requests[request_code] = data;
			this._requestCreateClientToPlugin(request_code);
		}
	} else {
		this._returnErrorToGotapiServer(404, 'Unknown request', data);
	}
};

GotapiInterface4.prototype._createNewRequestCode = function() {
	let id = '';
	id += mCrypto.randomBytes(32).toString('hex') + '_';
	id += Date.now();
	let sha256 = mCrypto.createHash('sha256');
	sha256.update(id);
	id = sha256.digest('hex');
	return id;
};

GotapiInterface4.prototype._requestServiceDiscovery = function(request_code) {
	let data = this.requests[request_code];
	let plugin_id_list = Object.keys(this.plugins);
	data['_plugin_id_list'] = plugin_id_list;
	data['services'] = [];
	this._requestServiceDiscoverySequentially(request_code);
};

GotapiInterface4.prototype._requestServiceDiscoverySequentially = function(request_code) {
	let data = this.requests[request_code];
	let plugin_id = data['_plugin_id_list'].shift();
	if(plugin_id) {
		data['_plugin_req_time'] = Date.now();
		this.plugins[plugin_id]['channel'].postMessage(data);
	} else {
		this._returnResponseToGotapiServer(data);
		delete this.requests[request_code];
	}
};

GotapiInterface4.prototype._requestCreateClientToPlugin = function(request_code) {
	let data = this.requests[request_code];
	let target_plugin_id = this._findPluginId(data);
	if(!target_plugin_id) {
		this._returnErrorToGotapiServer(404, 'Unknown request', data);
		delete this.requests[request_code];
		return;
	}
	let new_request_code = this._createNewRequestCode();
	let new_request_data = {
		receiver   : data['receiver'],
		requestCode: new_request_code,
		api        : data['api'],
		profile    : 'authorization',
		attribute  : 'createClient',
		package    : data['package'],
		_plugin_req_time: Date.now()
	};
	this.requests[new_request_code] = new_request_data;
	this.requests_approving[new_request_code] = data;
	this.plugins[target_plugin_id]['channel'].postMessage(new_request_data);
};

GotapiInterface4.prototype._requestAccessTokenToPlugin = function(request_code) {
	let data = this.requests[request_code];
	let target_plugin_id = this._findPluginId(this.requests_approving[request_code]);
	if(!target_plugin_id) {
		this._returnErrorToGotapiServer(404, 'Unknown request', data);
	}
	data['profile'] = 'authorization';
	data['attribute'] = 'requestAccessToken';
	data['_plugin_req_time'] = Date.now();
	this.plugins[target_plugin_id]['channel'].postMessage(data);
};

GotapiInterface4.prototype._requestCommandToPlugin = function(request_code) {
	let data = this.requests[request_code];
	let target_plugin_id = this._findPluginId(data);
	if(target_plugin_id) {
		this.plugins[target_plugin_id]['channel'].postMessage(data);
	} else {
		this._returnErrorToGotapiServer(404, 'Unknown request', data);
	}
};

GotapiInterface4.prototype._returnResponseToGotapiServer = function(data) {
	delete data['_plugin_req_time'];
	delete data['requestCode'];
	delete data['accessToken'];
	this.returnCallback(data);
};

GotapiInterface4.prototype._returnErrorToGotapiServer = function(code, message, data) {
	data['result'] = code;
	data['errorCode'] = code;
	data['errorMessage'] = message;
	this._returnResponseToGotapiServer(data);
};

GotapiInterface4.prototype._findPluginId = function(data) {
	let profile = data['profile'];
	let attribute = data['attribute'];
	let service_id = data['params']['serviceId'];
	let target_plugin_id = '';
	for(let id in this.plugins) {
		let plugin = this.plugins[id];
		let service_list = plugin['info']['services'];
		for(let i=0; i<service_list.length; i++) {
			let service = service_list[i];
			if(service['scopes'].indexOf(profile) >= 0 && service['serviceId'] === service_id) {
				target_plugin_id = id;
				break;
			}
		}
	}
	return target_plugin_id;
};

GotapiInterface4.prototype._receiveMessageFromPlugin = function(res) {
	if(res['action'] === 'EVENT') {
		this._receiveEventMessageFromPlugin(res);
	} else {
		this._receiveResponseMessageFromPlugin(res);
	}
}

GotapiInterface4.prototype._receiveEventMessageFromPlugin = function(res) {
	this._returnResponseToGotapiServer(res);
};

GotapiInterface4.prototype._receiveResponseMessageFromPlugin = function(res) {
	let request_code = res['requestCode'];
	let req = this.requests[request_code];
	if(!req) {
		return;
	}
	// Service Discovery
	if(req['profile'] === 'networkServiceDiscovery') {
		if(res['result'] === 0 && res['services'] && Array.isArray(res['services']) && res['services'].length > 0) {
			res['services'].forEach((s) => {
				req['services'].push(s);
			});
		}
		this._requestServiceDiscoverySequentially(request_code);
	// Plug-In authorization
	} else if(req['profile'] === 'authorization') {
		let orig_request_code = this.requests_approving[request_code]['requestCode'];
		let orig_request_data = this.requests[orig_request_code];
		if(orig_request_data) {
			// Created a client ID
			if(req['attribute'] === 'createClient') {
				if(res['result'] === 0 && res['clientId']) {
					req['clientId'] = res['clientId'];
					orig_request_data['clientId'] = res['clientId'];
					this._requestAccessTokenToPlugin(request_code);
				} else {
					let err_message = 'The Plug-In denied the request for a client ID.';
					if(res['errorMessage']) {
						err_message = res['errorMessage'];
					}
					this._returnErrorToGotapiServer(403, err_message, orig_request_data);
					delete this.requests[orig_request_code];
					delete this.requests[request_code];
					delete this.requests_approving[request_code];
				}
			// Created an access token
			} else if(req['attribute'] === 'requestAccessToken') {
				if(res['result'] === 0) {
					let client_id = orig_request_data['_client']['client_id'];
					if(!this.approved_clients[client_id]) {
						this.approved_clients[client_id] = {};
					}
					let service_id = orig_request_data['params']['serviceId'];
					this.approved_clients[client_id][service_id] = res['accessToken'];
					orig_request_data['accessToken'] = res['accessToken'];
					req['accessToken'] = res['accessToken'];
					delete this.requests[request_code];
					delete this.requests_approving[request_code];
					this._requestCommandToPlugin(orig_request_code);
				} else {
					let err_message = 'The Plug-In denied the request for an access token.';
					if(res['errorMessage']) {
						err_message = res['errorMessage'];
					}
					this._returnErrorToGotapiServer(403, err_message, orig_request_data);
					delete this.requests[orig_request_code];
					delete this.requests[request_code];
					delete this.requests_approving[request_code];
				}
			} else {
				this._returnErrorToGotapiServer(400, 'Unknown request.', orig_request_data);
				delete this.requests[orig_request_code];
				delete this.requests[request_code];
				delete this.requests_approving[request_code];
			}
		} else {
			delete this.requests[request_code];
			delete this.requests_approving[request_code];
			this._returnErrorToGotapiServer(400, 'Unknown request.', res);
		}
	} else {
		for(let k in req) {
			res[k] = req[k];
		}
		this._returnResponseToGotapiServer(res);
		delete this.requests[request_code];
	}
};

module.exports = GotapiInterface4;<commit_msg>Changed some variable declarations from "let" to "const"<commit_after>/* ------------------------------------------------------------------
* node-gotapi - gotapi-interface-4.js
*
* Copyright (c) 2017, Futomi Hatano, All rights reserved.
* Released under the MIT license
* Date: 2017-01-02
* ---------------------------------------------------------------- */
'use strict';
const mCrypto = require('crypto');
const mGotapiPluginLoader = require('./gotapi-plugin-loader.js');

/* ------------------------------------------------------------------
* Constructor: GotapiInterface4(config, returnCallback)
* ---------------------------------------------------------------- */
let GotapiInterface4 = function(config, returnCallback) {
	this.config = config;
	this.returnCallback = returnCallback;
	this.plugins = {};
	this.requests = {};
	this.requests_approving = {};
	this.approved_clients = {};
	this.plugin_response_timeout_watch_timer_id = 0;

	this.oncommunication = null;
};

/* ------------------------------------------------------------------
* Method: start()
* ---------------------------------------------------------------- */
GotapiInterface4.prototype.start = function() {
	var promise = new Promise((resolve, reject) => {
		// Find and load the GotAPI Plug-ins
		let plugin_loader = new mGotapiPluginLoader(this.config);
		plugin_loader.load((plugins) => {
			this.plugins = plugins;
			let list = [];
			for(let path in plugins) {
				let p = plugins[path];
				list.push(p);
				// Set a listener for `message` event fired on the message channel for the Plug-In
				p.channel.on('message', this._receiveMessageFromPlugin.bind(this));
				// Start to watch the timeout of the response from the Plug-In
				this.plugin_response_timeout_watch_timer_id = setInterval(() => {
					this._startWatchPluginResponseTimeout();
				}, 1000);
			}
			resolve(list);
		});
	});
	return promise;
};

GotapiInterface4.prototype._startWatchPluginResponseTimeout = function() {
	let now = Date.now();
	for(let request_code in this.requests) {
		let data = this.requests[request_code];
		let rtime = data['_plugin_req_time'];
		if(now - rtime > this.config.plugin_response_timeout * 1000) {
			data['result'] = 408;
			data['errorCode'] = 408;
			data['errorMessage'] = 'The Plug-In did not respond.';
			this._returnResponseToGotapiServer(data);
			delete this.requests[request_code];
		}
	}
};

/* ------------------------------------------------------------------
* Method: postMessage(data)
* This method is called by the GotAPI Server in order to send a
* message to the Plug-In.
* ---------------------------------------------------------------- */
GotapiInterface4.prototype.postMessage = function(data) {
	let request_code = this._createNewRequestCode();
	data['requestCode'] = request_code;
	data['_plugin_req_time'] = Date.now();
	let prof = data['profile'];
	let attr = data['attribute'];
	if(prof === 'servicediscovery') {
		data['profile'] = 'networkServiceDiscovery';
		data['attribute'] = 'getNetworkServices';
		this.requests[request_code] = data;
		this._requestServiceDiscovery(request_code);
	} else if(prof) {
		let client_id = data['_client']['client_id'];
		let service_id = data['params']['serviceId'];
		if(this.approved_clients[client_id] && this.approved_clients[client_id][service_id]) {
			data['accessToken'] = this.approved_clients[client_id][service_id];
			this.requests[request_code] = data;
			this._requestCommandToPlugin(request_code);
		} else {
			this.requests[request_code] = data;
			this._requestCreateClientToPlugin(request_code);
		}
	} else {
		this._returnErrorToGotapiServer(404, 'Unknown request', data);
	}
};

GotapiInterface4.prototype._createNewRequestCode = function() {
	let id = '';
	id += mCrypto.randomBytes(32).toString('hex') + '_';
	id += Date.now();
	let sha256 = mCrypto.createHash('sha256');
	sha256.update(id);
	id = sha256.digest('hex');
	return id;
};

GotapiInterface4.prototype._requestServiceDiscovery = function(request_code) {
	let data = this.requests[request_code];
	let plugin_id_list = Object.keys(this.plugins);
	data['_plugin_id_list'] = plugin_id_list;
	data['services'] = [];
	this._requestServiceDiscoverySequentially(request_code);
};

GotapiInterface4.prototype._requestServiceDiscoverySequentially = function(request_code) {
	let data = this.requests[request_code];
	let plugin_id = data['_plugin_id_list'].shift();
	if(plugin_id) {
		data['_plugin_req_time'] = Date.now();
		this.plugins[plugin_id]['channel'].postMessage(data);
	} else {
		this._returnResponseToGotapiServer(data);
		delete this.requests[request_code];
	}
};

GotapiInterface4.prototype._requestCreateClientToPlugin = function(request_code) {
	let data = this.requests[request_code];
	let target_plugin_id = this._findPluginId(data);
	if(!target_plugin_id) {
		this._returnErrorToGotapiServer(404, 'Unknown request', data);
		delete this.requests[request_code];
		return;
	}
	let new_request_code = this._createNewRequestCode();
	let new_request_data = {
		receiver   : data['receiver'],
		requestCode: new_request_code,
		api        : data['api'],
		profile    : 'authorization',
		attribute  : 'createClient',
		package    : data['package'],
		_plugin_req_time: Date.now()
	};
	this.requests[new_request_code] = new_request_data;
	this.requests_approving[new_request_code] = data;
	this.plugins[target_plugin_id]['channel'].postMessage(new_request_data);
};

GotapiInterface4.prototype._requestAccessTokenToPlugin = function(request_code) {
	let data = this.requests[request_code];
	let target_plugin_id = this._findPluginId(this.requests_approving[request_code]);
	if(!target_plugin_id) {
		this._returnErrorToGotapiServer(404, 'Unknown request', data);
	}
	data['profile'] = 'authorization';
	data['attribute'] = 'requestAccessToken';
	data['_plugin_req_time'] = Date.now();
	this.plugins[target_plugin_id]['channel'].postMessage(data);
};

GotapiInterface4.prototype._requestCommandToPlugin = function(request_code) {
	let data = this.requests[request_code];
	let target_plugin_id = this._findPluginId(data);
	if(target_plugin_id) {
		this.plugins[target_plugin_id]['channel'].postMessage(data);
	} else {
		this._returnErrorToGotapiServer(404, 'Unknown request', data);
	}
};

GotapiInterface4.prototype._returnResponseToGotapiServer = function(data) {
	delete data['_plugin_req_time'];
	delete data['requestCode'];
	delete data['accessToken'];
	this.returnCallback(data);
};

GotapiInterface4.prototype._returnErrorToGotapiServer = function(code, message, data) {
	data['result'] = code;
	data['errorCode'] = code;
	data['errorMessage'] = message;
	this._returnResponseToGotapiServer(data);
};

GotapiInterface4.prototype._findPluginId = function(data) {
	let profile = data['profile'];
	let attribute = data['attribute'];
	let service_id = data['params']['serviceId'];
	let target_plugin_id = '';
	for(let id in this.plugins) {
		let plugin = this.plugins[id];
		let service_list = plugin['info']['services'];
		for(let i=0; i<service_list.length; i++) {
			let service = service_list[i];
			if(service['scopes'].indexOf(profile) >= 0 && service['serviceId'] === service_id) {
				target_plugin_id = id;
				break;
			}
		}
	}
	return target_plugin_id;
};

GotapiInterface4.prototype._receiveMessageFromPlugin = function(res) {
	if(res['action'] === 'EVENT') {
		this._receiveEventMessageFromPlugin(res);
	} else {
		this._receiveResponseMessageFromPlugin(res);
	}
}

GotapiInterface4.prototype._receiveEventMessageFromPlugin = function(res) {
	this._returnResponseToGotapiServer(res);
};

GotapiInterface4.prototype._receiveResponseMessageFromPlugin = function(res) {
	let request_code = res['requestCode'];
	let req = this.requests[request_code];
	if(!req) {
		return;
	}
	// Service Discovery
	if(req['profile'] === 'networkServiceDiscovery') {
		if(res['result'] === 0 && res['services'] && Array.isArray(res['services']) && res['services'].length > 0) {
			res['services'].forEach((s) => {
				req['services'].push(s);
			});
		}
		this._requestServiceDiscoverySequentially(request_code);
	// Plug-In authorization
	} else if(req['profile'] === 'authorization') {
		let orig_request_code = this.requests_approving[request_code]['requestCode'];
		let orig_request_data = this.requests[orig_request_code];
		if(orig_request_data) {
			// Created a client ID
			if(req['attribute'] === 'createClient') {
				if(res['result'] === 0 && res['clientId']) {
					req['clientId'] = res['clientId'];
					orig_request_data['clientId'] = res['clientId'];
					this._requestAccessTokenToPlugin(request_code);
				} else {
					let err_message = 'The Plug-In denied the request for a client ID.';
					if(res['errorMessage']) {
						err_message = res['errorMessage'];
					}
					this._returnErrorToGotapiServer(403, err_message, orig_request_data);
					delete this.requests[orig_request_code];
					delete this.requests[request_code];
					delete this.requests_approving[request_code];
				}
			// Created an access token
			} else if(req['attribute'] === 'requestAccessToken') {
				if(res['result'] === 0) {
					let client_id = orig_request_data['_client']['client_id'];
					if(!this.approved_clients[client_id]) {
						this.approved_clients[client_id] = {};
					}
					let service_id = orig_request_data['params']['serviceId'];
					this.approved_clients[client_id][service_id] = res['accessToken'];
					orig_request_data['accessToken'] = res['accessToken'];
					req['accessToken'] = res['accessToken'];
					delete this.requests[request_code];
					delete this.requests_approving[request_code];
					this._requestCommandToPlugin(orig_request_code);
				} else {
					let err_message = 'The Plug-In denied the request for an access token.';
					if(res['errorMessage']) {
						err_message = res['errorMessage'];
					}
					this._returnErrorToGotapiServer(403, err_message, orig_request_data);
					delete this.requests[orig_request_code];
					delete this.requests[request_code];
					delete this.requests_approving[request_code];
				}
			} else {
				this._returnErrorToGotapiServer(400, 'Unknown request.', orig_request_data);
				delete this.requests[orig_request_code];
				delete this.requests[request_code];
				delete this.requests_approving[request_code];
			}
		} else {
			delete this.requests[request_code];
			delete this.requests_approving[request_code];
			this._returnErrorToGotapiServer(400, 'Unknown request.', res);
		}
	} else {
		for(let k in req) {
			res[k] = req[k];
		}
		this._returnResponseToGotapiServer(res);
		delete this.requests[request_code];
	}
};

module.exports = GotapiInterface4;<|endoftext|>


<commit_before>/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -  */
/* Handlebars helpers for setting selected option and checked checkboxes / radio buttons          */
/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -  */

'use strict';

const jsdom = require('jsdom').jsdom; // JavaScript implementation of DOM and HTML standards

class HandlebarsHelpers {

    static checked(value, options) {
        const document = jsdom();
        const div = document.createElement('div'); // create a container div
        div.innerHTML = options.fn(this);          // parse content into dom
        if (typeof value == 'string') {
            div.querySelectorAll('input[type=radio],input[type=checkbox]').forEach(function(input) {
                // if input value matches supplied value, check it
                if (input.value == value) input.defaultChecked = true;
            });
        }
        if (typeof value == 'object') {
            div.querySelectorAll('input[type=checkbox]').forEach(function(input) {
                // if input value is included in supplied value, check it
                if (value.includes(input.value)) input.defaultChecked = true;
            });
        }
        return div.innerHTML;
    }

    static selected(value, options) {   // stackoverflow.com/questions/13046401#answer-15373215
        const document = jsdom();
        const select = document.createElement('select'); // create a select element
        select.innerHTML = options.fn(this);             // populate it with the option HTML
        select.value = value;                            // set the value
        if (select.children[select.selectedIndex]) {     // if selected node exists add 'selected' attribute
            select.children[select.selectedIndex].setAttribute('selected', '');
        }
        return select.innerHTML;
    }
}

/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -  */

module.exports = HandlebarsHelpers;
<commit_msg>Bump dependencies (missed jsdom)<commit_after>/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -  */
/* Handlebars helpers for setting selected option and checked checkboxes / radio buttons          */
/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -  */

'use strict';

const jsdom = require('jsdom'); // DOM Document interface in Node!

const document = new jsdom.JSDOM().window.document;

class HandlebarsHelpers {

    static checked(value, options) {
        const div = document.createElement('div'); // create a container div
        div.innerHTML = options.fn(this);          // parse content into dom
        if (typeof value == 'string') {
            div.querySelectorAll('input[type=radio],input[type=checkbox]').forEach(function(input) {
                // if input value matches supplied value, check it
                if (input.value == value) input.defaultChecked = true;
            });
        }
        if (typeof value == 'object') {
            div.querySelectorAll('input[type=checkbox]').forEach(function(input) {
                // if input value is included in supplied value, check it
                if (value.includes(input.value)) input.defaultChecked = true;
            });
        }
        return div.innerHTML;
    }

    static selected(value, options) {   // stackoverflow.com/questions/13046401#answer-15373215
        const select = document.createElement('select'); // create a select element
        select.innerHTML = options.fn(this);             // populate it with the option HTML
        select.value = value;                            // set the value
        if (select.children[select.selectedIndex]) {     // if selected node exists add 'selected' attribute
            select.children[select.selectedIndex].setAttribute('selected', '');
        }
        return select.innerHTML;
    }
}

/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -  */

module.exports = HandlebarsHelpers;
<|endoftext|>


<commit_before>/**
 * This module loads the handlebars template engine and adds custom extensions
 * Those are mainly helper functions that make wikitext generation easier
 */

var handlebars = require('handlebars');

//////////////////////////////////////////
// Handlebars Custom Extensions         //
//////////////////////////////////////////

/**
 * Generate SMW Template Attribute
 */
handlebars.registerHelper('smw_templ_attr', function(name) {
    var result = '[[' + name + '::{{{' + name + '|}}}]]';
    return new handlebars.SafeString(result);
});

/**
 * Generate ArrayMap Template
 */
handlebars.registerHelper('smw_templ_array', function(name) {
    var result = '{{#arraymap:{{{' + name + '|}}}|;|x|[[' + name + '::x]]|, }}';
    return new handlebars.SafeString(result);
});

/**
 * Convert JavaScript Object (Set) to parameters
 * If no param object is given, this returns ''
 */
handlebars.registerHelper('smw_params', function(params) {

    var wikitext = '';

    if (params) {
        for (var paramKey in params) {
            var paramValue = params[paramKey];
            if (!paramValue || paramValue === true) {
                wikitext += '|' + paramKey;
            } else {
                wikitext += '|' + paramKey + '=' + paramValue;
            }
        }
    }

    return new handlebars.SafeString(wikitext);
});

/**
 * Adds further parameters to the Form Header
 */
handlebars.registerHelper('smw_formparams', function(multiple) {

    var result = '';

    if (multiple) {
        result += '|multiple';

        if (multiple.minItems || multiple.minItems === 0) {
            result += '|minimum instances=' + multiple.minItems;
        }

        if (multiple.maxItems) {
            result += '|maximum instances=' + multiple.maxItems;
        }
    }
    return new handlebars.SafeString(result);
});

/**
 * Generate SMW Form Entry
 */
handlebars.registerHelper('smw_formtable', function(model) {

    var result = '';

    if (model && model.properties) {

        var attrCollection = model.properties;

        result += '{| class="formtable"\n';

        for (var attrName in attrCollection) {

            // Variables
            var attr = attrCollection[attrName];
            var paramObj = {};

            // Reset default Size (Set more intelligently by CSS)
            paramObj['size'] = 'none';

            // Default Widget Type
            paramObj['input type'] = 'text with autocomplete';

            result += '! ' + attr.title + ':\n';
            result += '| {{{field|' + attrName;

            // Mandatory Fields
            if (model.required && model.required.indexOf(attrName) > -1) {
                paramObj.mandatory = true;
            }

            // Enum Fields
            if (attr.enum) {
                paramObj['input type'] = 'dropdown';
                paramObj['values'] = attr.enum.join(', ');
            }

            // Multiple Values -> List
            if (attr.type === 'array') {
                paramObj['list'] = true;
                paramObj['delimiter'] = ';';
                paramObj['input type'] = 'tokens';
            }

            // Booleans
            if (attr.type === 'boolean') {
                paramObj['input type'] = 'checkbox';
            }

            if (attr.format && attr.format === 'date') {
                paramObj['input type'] = 'datepicker';
            }

            // Default Values
            if (attr.default) {
                paramObj['default'] = attr.default;
            }

            // Default Values
            if (attr.size) {
                paramObj['size'] = attr.size;
            }

            // JSON Schema Validation
            if (attr.maxLength) {
                paramObj['maxlength'] = attr.maxLength;
            }

            // If attributes are given in smw_form use them directly. Overwrites all calculated parameters
            if (attr.smw_form) {
                for (var paramName in attr.smw_form) {
                    paramObj[paramName] = attr.smw_form[paramName];
                }
            }

            if (paramObj['class']) {
                paramObj['class'] += ' attr_' + attrName;
            } else {
                paramObj['class'] = 'attr_' + attrName;
            }

            result += exports.addParameters(paramObj);

            result += '}}}';

            // Hack to insert Field Description
            if (attr.description) {
                result += '{{#info:' + attr.description + '}}';
            }

            result += '\n|-\n';
        }

        result += '|}\n';
    } else {
        result += '';
    }

    return new handlebars.SafeString(result);
});

/**
 * Helper Function wich takes a Parameter Object and turns it into wikitext parameters
 *
 * @param paramObj
 * @returns {string}
 */
exports.addParameters = function(paramObj, lineBreak) {
    var html = '';
    for (var paramName in paramObj) {
        var paramValue = paramObj[paramName];

        if (paramValue === true) {
            html += '|' + paramName;
        } else if (paramValue === false) {
            // Skip Attribute
        } else {
            html += '|' + paramName + '=' + paramValue;
        }
        if (lineBreak) {
            html += '\n';
        }

    }

    return html;

};

module.exports = handlebars;
<commit_msg>Support for recommended array. CSS is required to make this visible.<commit_after>/**
 * This module loads the handlebars template engine and adds custom extensions
 * Those are mainly helper functions that make wikitext generation easier
 */

var handlebars = require('handlebars');

//////////////////////////////////////////
// Handlebars Custom Extensions         //
//////////////////////////////////////////

/**
 * Generate SMW Template Attribute
 */
handlebars.registerHelper('smw_templ_attr', function(name) {
    var result = '[[' + name + '::{{{' + name + '|}}}]]';
    return new handlebars.SafeString(result);
});

/**
 * Generate ArrayMap Template
 */
handlebars.registerHelper('smw_templ_array', function(name) {
    var result = '{{#arraymap:{{{' + name + '|}}}|;|x|[[' + name + '::x]]|, }}';
    return new handlebars.SafeString(result);
});

/**
 * Convert JavaScript Object (Set) to parameters
 * If no param object is given, this returns ''
 */
handlebars.registerHelper('smw_params', function(params) {

    var wikitext = '';

    if (params) {
        for (var paramKey in params) {
            var paramValue = params[paramKey];
            if (!paramValue || paramValue === true) {
                wikitext += '|' + paramKey;
            } else {
                wikitext += '|' + paramKey + '=' + paramValue;
            }
        }
    }

    return new handlebars.SafeString(wikitext);
});

/**
 * Adds further parameters to the Form Header
 */
handlebars.registerHelper('smw_formparams', function(multiple) {

    var result = '';

    if (multiple) {
        result += '|multiple';

        if (multiple.minItems || multiple.minItems === 0) {
            result += '|minimum instances=' + multiple.minItems;
        }

        if (multiple.maxItems) {
            result += '|maximum instances=' + multiple.maxItems;
        }
    }
    return new handlebars.SafeString(result);
});

/**
 * Generate SMW Form Entry
 */
handlebars.registerHelper('smw_formtable', function(model) {

    var result = '';

    if (model && model.properties) {

        var attrCollection = model.properties;

        result += '{| class="formtable"\n';

        for (var attrName in attrCollection) {

            // Variables
            var attr = attrCollection[attrName];
            var paramObj = {};

            // Reset default Size (Set more intelligently by CSS)
            paramObj['size'] = 'none';

            // Tag field via html class with attribute Name
            paramObj['class'] = 'attr_' + attrName;

            // Default Widget Type
            paramObj['input type'] = 'text with autocomplete';

            result += '! ' + attr.title + ':\n';
            result += '| {{{field|' + attrName;

            // Mandatory Fields
            if (model.required && model.required.indexOf(attrName) > -1) {
                paramObj.mandatory = true;
            }

            // Recommended Fields
            if (model.recommended && model.recommended.indexOf(attrName) > -1) {
                paramObj['class'] += ' recommendedField';
            }

            // Enum Fields
            if (attr.enum) {
                paramObj['input type'] = 'dropdown';
                paramObj['values'] = attr.enum.join(', ');
            }

            // Multiple Values -> List
            if (attr.type === 'array') {
                paramObj['list'] = true;
                paramObj['delimiter'] = ';';
                paramObj['input type'] = 'tokens';
            }

            // Booleans
            if (attr.type === 'boolean') {
                paramObj['input type'] = 'checkbox';
            }

            if (attr.format && attr.format === 'date') {
                paramObj['input type'] = 'datepicker';
            }

            // Default Values
            if (attr.default) {
                paramObj['default'] = attr.default;
            }

            // Default Values
            if (attr.size) {
                paramObj['size'] = attr.size;
            }

            // JSON Schema Validation
            if (attr.maxLength) {
                paramObj['maxlength'] = attr.maxLength;
            }

            // If attributes are given in smw_form use them directly. Overwrites all calculated parameters
            if (attr.smw_form) {
                for (var paramName in attr.smw_form) {
                    paramObj[paramName] = attr.smw_form[paramName];
                }
            }

            result += exports.addParameters(paramObj);

            result += '}}}';

            // Hack to insert Field Description
            if (attr.description) {
                result += '{{#info:' + attr.description + '}}';
            }

            result += '\n|-\n';
        }

        result += '|}\n';
    } else {
        result += '';
    }

    return new handlebars.SafeString(result);
});

/**
 * Helper Function wich takes a Parameter Object and turns it into wikitext parameters
 *
 * @param paramObj
 * @returns {string}
 */
exports.addParameters = function(paramObj, lineBreak) {
    var html = '';
    for (var paramName in paramObj) {
        var paramValue = paramObj[paramName];

        if (paramValue === true) {
            html += '|' + paramName;
        } else if (paramValue === false) {
            // Skip Attribute
        } else {
            html += '|' + paramName + '=' + paramValue;
        }
        if (lineBreak) {
            html += '\n';
        }

    }

    return html;

};

module.exports = handlebars;
<|endoftext|>