Ecoer Logo

@davidpm

44

quick posts about programming

steemit.com/@davidpm
VOTING POWER100.00%
DOWNVOTE POWER100.00%
RESOURCE CREDITS100.00%
REPUTATION PROGRESS26.99%
Net Worth
6.449USD
STEEM
0.035STEEM
SBD
13.416SBD
Effective Power
5.010SP
├── Own SP
0.126SP
└── Incoming Deleg
+4.884SP

Detailed Balance

STEEM
balance
0.001STEEM
market_balance
0.000STEEM
savings_balance
0.000STEEM
reward_steem_balance
0.034STEEM
STEEM POWER
Own SP
0.126SP
Delegated Out
0.000SP
Delegation In
4.884SP
Effective Power
5.010SP
Reward SP (pending)
8.422SP
SBD
sbd_balance
0.000SBD
sbd_conversions
0.000SBD
sbd_market_balance
0.000SBD
savings_sbd_balance
0.000SBD
reward_sbd_balance
13.416SBD
{
  "balance": "0.001 STEEM",
  "savings_balance": "0.000 STEEM",
  "reward_steem_balance": "0.034 STEEM",
  "vesting_shares": "204.281357 VESTS",
  "delegated_vesting_shares": "0.000000 VESTS",
  "received_vesting_shares": "7939.378449 VESTS",
  "sbd_balance": "0.000 SBD",
  "savings_sbd_balance": "0.000 SBD",
  "reward_sbd_balance": "13.416 SBD",
  "conversions": []
}

Account Info

namedavidpm
id800148
rank521,963
reputation138388664591
created2018-03-03T02:46:06
recovery_accountsteem
proxyNone
post_count17
comment_count0
lifetime_vote_count0
witnesses_voted_for0
last_post2019-08-09T03:01:57
last_root_post2019-08-09T03:01:57
last_vote_time2018-07-24T18:25:51
proxied_vsf_votes0, 0, 0, 0
can_vote1
voting_power0
delayed_votes0
balance0.001 STEEM
savings_balance0.000 STEEM
sbd_balance0.000 SBD
savings_sbd_balance0.000 SBD
vesting_shares204.281357 VESTS
delegated_vesting_shares0.000000 VESTS
received_vesting_shares7939.378449 VESTS
reward_vesting_balance17190.776628 VESTS
vesting_balance0.000 STEEM
vesting_withdraw_rate0.000000 VESTS
next_vesting_withdrawal1969-12-31T23:59:59
withdrawn0
to_withdraw0
withdraw_routes0
savings_withdraw_requests0
last_account_recovery1970-01-01T00:00:00
reset_accountnull
last_owner_update1970-01-01T00:00:00
last_account_update2018-03-05T01:37:21
minedNo
sbd_seconds0
sbd_last_interest_payment1970-01-01T00:00:00
savings_sbd_last_interest_payment1970-01-01T00:00:00
{
  "active": {
    "account_auths": [],
    "key_auths": [
      [
        "STM8kxLaTeG6HXYMuVtjr6R1yFcDpob1H4R7HuWj3CpQDKB2rhRnZ",
        1
      ]
    ],
    "weight_threshold": 1
  },
  "balance": "0.001 STEEM",
  "can_vote": true,
  "comment_count": 0,
  "created": "2018-03-03T02:46:06",
  "curation_rewards": 1,
  "delegated_vesting_shares": "0.000000 VESTS",
  "downvote_manabar": {
    "current_mana": 2035914951,
    "last_update_time": 1779059865
  },
  "guest_bloggers": [],
  "id": 800148,
  "json_metadata": "{\"profile\":{\"profile_image\":\"https://avatars3.githubusercontent.com/u/6234571?s=400&u=81563e5c8010d6ffdb26be7d77dad7c555c6066f&v=4\",\"website\":\"http://davidpm.io\",\"cover_image\":\"https://pbs.twimg.com/profile_banners/948220550/1503333176/1500x500\",\"about\":\"quick posts about programming\"}}",
  "last_account_recovery": "1970-01-01T00:00:00",
  "last_account_update": "2018-03-05T01:37:21",
  "last_owner_update": "1970-01-01T00:00:00",
  "last_post": "2019-08-09T03:01:57",
  "last_root_post": "2019-08-09T03:01:57",
  "last_vote_time": "2018-07-24T18:25:51",
  "lifetime_vote_count": 0,
  "market_history": [],
  "memo_key": "STM6P3LvVZjSRnWetfbWcXeEcF6smw5xpW87s43oEd21Lea53ovPb",
  "mined": false,
  "name": "davidpm",
  "next_vesting_withdrawal": "1969-12-31T23:59:59",
  "other_history": [],
  "owner": {
    "account_auths": [],
    "key_auths": [
      [
        "STM5DEkfgR4JvnCFgdPyqbyseWJS4ePqHj9VnBbLVadAJuCQErBpy",
        1
      ]
    ],
    "weight_threshold": 1
  },
  "pending_claimed_accounts": 0,
  "post_bandwidth": 0,
  "post_count": 17,
  "post_history": [],
  "posting": {
    "account_auths": [],
    "key_auths": [
      [
        "STM5D1ZCtab5A5DzioThZSGKCASa4QuYXsegaFYr4E2edcKa85ciP",
        1
      ]
    ],
    "weight_threshold": 1
  },
  "posting_json_metadata": "{\"profile\":{\"profile_image\":\"https://avatars3.githubusercontent.com/u/6234571?s=400&u=81563e5c8010d6ffdb26be7d77dad7c555c6066f&v=4\",\"website\":\"http://davidpm.io\",\"cover_image\":\"https://pbs.twimg.com/profile_banners/948220550/1503333176/1500x500\",\"about\":\"quick posts about programming\"}}",
  "posting_rewards": 10904,
  "proxied_vsf_votes": [
    0,
    0,
    0,
    0
  ],
  "proxy": "",
  "received_vesting_shares": "7939.378449 VESTS",
  "recovery_account": "steem",
  "reputation": "138388664591",
  "reset_account": "null",
  "reward_sbd_balance": "13.416 SBD",
  "reward_steem_balance": "0.034 STEEM",
  "reward_vesting_balance": "17190.776628 VESTS",
  "reward_vesting_steem": "8.422 STEEM",
  "savings_balance": "0.000 STEEM",
  "savings_sbd_balance": "0.000 SBD",
  "savings_sbd_last_interest_payment": "1970-01-01T00:00:00",
  "savings_sbd_seconds": "0",
  "savings_sbd_seconds_last_update": "1970-01-01T00:00:00",
  "savings_withdraw_requests": 0,
  "sbd_balance": "0.000 SBD",
  "sbd_last_interest_payment": "1970-01-01T00:00:00",
  "sbd_seconds": "0",
  "sbd_seconds_last_update": "1970-01-01T00:00:00",
  "tags_usage": [],
  "to_withdraw": 0,
  "transfer_history": [],
  "vesting_balance": "0.000 STEEM",
  "vesting_shares": "204.281357 VESTS",
  "vesting_withdraw_rate": "0.000000 VESTS",
  "vote_history": [],
  "voting_manabar": {
    "current_mana": "8143659806",
    "last_update_time": 1779059865
  },
  "voting_power": 0,
  "withdraw_routes": 0,
  "withdrawn": 0,
  "witness_votes": [],
  "witnesses_voted_for": 0,
  "rank": 521963
}

Withdraw Routes

IncomingOutgoing
Empty
Empty
{
  "incoming": [],
  "outgoing": []
}
From Date
To Date
steemdelegated 4.884 SP to @davidpm
2026/05/17 23:17:45
delegateedavidpm
delegatorsteem
vesting shares7939.378449 VESTS
Transaction InfoBlock #106142304/Trx 4a92b6b4c77a5f74d992f6df28d272b2cd6808ec
View Raw JSON Data
{
  "block": 106142304,
  "op": [
    "delegate_vesting_shares",
    {
      "delegatee": "davidpm",
      "delegator": "steem",
      "vesting_shares": "7939.378449 VESTS"
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2026-05-17T23:17:45",
  "trx_id": "4a92b6b4c77a5f74d992f6df28d272b2cd6808ec",
  "trx_in_block": 1,
  "virtual_op": 0
}
steemdelegated 3.216 SP to @davidpm
2026/05/11 23:58:51
delegateedavidpm
delegatorsteem
vesting shares5227.168044 VESTS
Transaction InfoBlock #105971089/Trx 6b9abc42fbeccecffadae72eeaee6605ca847fe5
View Raw JSON Data
{
  "block": 105971089,
  "op": [
    "delegate_vesting_shares",
    {
      "delegatee": "davidpm",
      "delegator": "steem",
      "vesting_shares": "5227.168044 VESTS"
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2026-05-11T23:58:51",
  "trx_id": "6b9abc42fbeccecffadae72eeaee6605ca847fe5",
  "trx_in_block": 1,
  "virtual_op": 0
}
steemdelegated 4.892 SP to @davidpm
2026/04/25 22:40:18
delegateedavidpm
delegatorsteem
vesting shares7951.894205 VESTS
Transaction InfoBlock #105509986/Trx 6fffc6d3538dce58a71d50c4e87ef26add073536
View Raw JSON Data
{
  "block": 105509986,
  "op": [
    "delegate_vesting_shares",
    {
      "delegatee": "davidpm",
      "delegator": "steem",
      "vesting_shares": "7951.894205 VESTS"
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2026-04-25T22:40:18",
  "trx_id": "6fffc6d3538dce58a71d50c4e87ef26add073536",
  "trx_in_block": 0,
  "virtual_op": 0
}
steemdelegated 3.241 SP to @davidpm
2026/01/23 05:13:48
delegateedavidpm
delegatorsteem
vesting shares5268.714863 VESTS
Transaction InfoBlock #102848729/Trx f7d1a0a8405940875b1ad8fb547879a54f0fc156
View Raw JSON Data
{
  "block": 102848729,
  "op": [
    "delegate_vesting_shares",
    {
      "delegatee": "davidpm",
      "delegator": "steem",
      "vesting_shares": "5268.714863 VESTS"
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2026-01-23T05:13:48",
  "trx_id": "f7d1a0a8405940875b1ad8fb547879a54f0fc156",
  "trx_in_block": 1,
  "virtual_op": 0
}
steemdelegated 3.342 SP to @davidpm
2024/12/17 00:33:45
delegateedavidpm
delegatorsteem
vesting shares5432.934060 VESTS
Transaction InfoBlock #91295154/Trx 62ce356d3c56462f7e159dfcb5e5e0c28bee9ffb
View Raw JSON Data
{
  "block": 91295154,
  "op": [
    "delegate_vesting_shares",
    {
      "delegatee": "davidpm",
      "delegator": "steem",
      "vesting_shares": "5432.934060 VESTS"
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2024-12-17T00:33:45",
  "trx_id": "62ce356d3c56462f7e159dfcb5e5e0c28bee9ffb",
  "trx_in_block": 0,
  "virtual_op": 0
}
steemdelegated 3.446 SP to @davidpm
2023/11/13 16:17:24
delegateedavidpm
delegatorsteem
vesting shares5602.067592 VESTS
Transaction InfoBlock #79849384/Trx 7392d75e78f0004367daa5e2f7851c125fda2eee
View Raw JSON Data
{
  "block": 79849384,
  "op": [
    "delegate_vesting_shares",
    {
      "delegatee": "davidpm",
      "delegator": "steem",
      "vesting_shares": "5602.067592 VESTS"
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2023-11-13T16:17:24",
  "trx_id": "7392d75e78f0004367daa5e2f7851c125fda2eee",
  "trx_in_block": 6,
  "virtual_op": 0
}
steemdelegated 5.253 SP to @davidpm
2023/09/21 20:42:18
delegateedavidpm
delegatorsteem
vesting shares8539.346378 VESTS
Transaction InfoBlock #78346484/Trx 9d116f69c7516aab7c61a3d564e452c28f83d82f
View Raw JSON Data
{
  "block": 78346484,
  "op": [
    "delegate_vesting_shares",
    {
      "delegatee": "davidpm",
      "delegator": "steem",
      "vesting_shares": "8539.346378 VESTS"
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2023-09-21T20:42:18",
  "trx_id": "9d116f69c7516aab7c61a3d564e452c28f83d82f",
  "trx_in_block": 1,
  "virtual_op": 0
}
steemdelegated 5.389 SP to @davidpm
2022/11/03 10:38:36
delegateedavidpm
delegatorsteem
vesting shares8761.027816 VESTS
Transaction InfoBlock #69111996/Trx 2bdcc6415336babe8ee7f3c101b5c557a7a10070
View Raw JSON Data
{
  "block": 69111996,
  "op": [
    "delegate_vesting_shares",
    {
      "delegatee": "davidpm",
      "delegator": "steem",
      "vesting_shares": "8761.027816 VESTS"
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2022-11-03T10:38:36",
  "trx_id": "2bdcc6415336babe8ee7f3c101b5c557a7a10070",
  "trx_in_block": 2,
  "virtual_op": 0
}
steemdelegated 5.525 SP to @davidpm
2022/01/17 09:59:48
delegateedavidpm
delegatorsteem
vesting shares8981.561047 VESTS
Transaction InfoBlock #60808268/Trx d55a51ae09560ce2d6fb79c21c43dcc8ffb262cd
View Raw JSON Data
{
  "block": 60808268,
  "op": [
    "delegate_vesting_shares",
    {
      "delegatee": "davidpm",
      "delegator": "steem",
      "vesting_shares": "8981.561047 VESTS"
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2022-01-17T09:59:48",
  "trx_id": "d55a51ae09560ce2d6fb79c21c43dcc8ffb262cd",
  "trx_in_block": 19,
  "virtual_op": 0
}
steemdelegated 5.638 SP to @davidpm
2021/06/13 23:57:18
delegateedavidpm
delegatorsteem
vesting shares9165.329705 VESTS
Transaction InfoBlock #54606703/Trx c491496d4f459e144a687a30727f5f6c44d80532
View Raw JSON Data
{
  "block": 54606703,
  "op": [
    "delegate_vesting_shares",
    {
      "delegatee": "davidpm",
      "delegator": "steem",
      "vesting_shares": "9165.329705 VESTS"
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2021-06-13T23:57:18",
  "trx_id": "c491496d4f459e144a687a30727f5f6c44d80532",
  "trx_in_block": 0,
  "virtual_op": 0
}
steemdelegated 5.753 SP to @davidpm
2020/12/11 10:17:39
delegateedavidpm
delegatorsteem
vesting shares9352.751679 VESTS
Transaction InfoBlock #49354199/Trx 39495d1fa3fa19b2d30085d5dcaafe139b25c46e
View Raw JSON Data
{
  "block": 49354199,
  "op": [
    "delegate_vesting_shares",
    {
      "delegatee": "davidpm",
      "delegator": "steem",
      "vesting_shares": "9352.751679 VESTS"
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2020-12-11T10:17:39",
  "trx_id": "39495d1fa3fa19b2d30085d5dcaafe139b25c46e",
  "trx_in_block": 4,
  "virtual_op": 0
}
steemdelegated 1.177 SP to @davidpm
2020/12/06 03:54:42
delegateedavidpm
delegatorsteem
vesting shares1912.543513 VESTS
Transaction InfoBlock #49205761/Trx 637780728a1a52138df2e7404c135c6d4a3f34a6
View Raw JSON Data
{
  "block": 49205761,
  "op": [
    "delegate_vesting_shares",
    {
      "delegatee": "davidpm",
      "delegator": "steem",
      "vesting_shares": "1912.543513 VESTS"
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2020-12-06T03:54:42",
  "trx_id": "637780728a1a52138df2e7404c135c6d4a3f34a6",
  "trx_in_block": 2,
  "virtual_op": 0
}
steemdelegated 5.757 SP to @davidpm
2020/12/05 11:52:03
delegateedavidpm
delegatorsteem
vesting shares9359.118318 VESTS
Transaction InfoBlock #49186871/Trx ed36edc5d88474a63d3b9eb3b3824be877b5ce52
View Raw JSON Data
{
  "block": 49186871,
  "op": [
    "delegate_vesting_shares",
    {
      "delegatee": "davidpm",
      "delegator": "steem",
      "vesting_shares": "9359.118318 VESTS"
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2020-12-05T11:52:03",
  "trx_id": "ed36edc5d88474a63d3b9eb3b3824be877b5ce52",
  "trx_in_block": 5,
  "virtual_op": 0
}
steemdelegated 1.181 SP to @davidpm
2020/11/02 13:51:33
delegateedavidpm
delegatorsteem
vesting shares1920.017158 VESTS
Transaction InfoBlock #48255710/Trx a33ccca58d377c2e75b33825f9ca2e22d976af67
View Raw JSON Data
{
  "block": 48255710,
  "op": [
    "delegate_vesting_shares",
    {
      "delegatee": "davidpm",
      "delegator": "steem",
      "vesting_shares": "1920.017158 VESTS"
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2020-11-02T13:51:33",
  "trx_id": "a33ccca58d377c2e75b33825f9ca2e22d976af67",
  "trx_in_block": 2,
  "virtual_op": 0
}
steemdelegated 5.882 SP to @davidpm
2020/05/09 04:51:15
delegateedavidpm
delegatorsteem
vesting shares9561.764892 VESTS
Transaction InfoBlock #43216000/Trx ec8bbff692dd16bd04c90eea9ea63b9764cbc7f4
View Raw JSON Data
{
  "block": 43216000,
  "op": [
    "delegate_vesting_shares",
    {
      "delegatee": "davidpm",
      "delegator": "steem",
      "vesting_shares": "9561.764892 VESTS"
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2020-05-09T04:51:15",
  "trx_id": "ec8bbff692dd16bd04c90eea9ea63b9764cbc7f4",
  "trx_in_block": 6,
  "virtual_op": 0
}
steemdelegated 1.202 SP to @davidpm
2020/05/08 08:19:36
delegateedavidpm
delegatorsteem
vesting shares1953.311140 VESTS
Transaction InfoBlock #43191943/Trx 7ce1c8ad8ed7594fd832029ab485af3e3c7a441b
View Raw JSON Data
{
  "block": 43191943,
  "op": [
    "delegate_vesting_shares",
    {
      "delegatee": "davidpm",
      "delegator": "steem",
      "vesting_shares": "1953.311140 VESTS"
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2020-05-08T08:19:36",
  "trx_id": "7ce1c8ad8ed7594fd832029ab485af3e3c7a441b",
  "trx_in_block": 2,
  "virtual_op": 0
}
2020/03/05 03:34:30
authorsteemitboard
bodyCongratulations @davidpm! You received a personal award! <table><tr><td>https://steemitimages.com/70x70/http://steemitboard.com/@davidpm/birthday2.png</td><td>Happy Birthday! - You are on the Steem blockchain for 2 years!</td></tr></table> <sub>_You can view [your badges on your Steem Board](https://steemitboard.com/@davidpm) and compare to others on the [Steem Ranking](https://steemitboard.com/ranking/index.php?name=davidpm)_</sub> **Do not miss the last post from @steemitboard:** <table><tr><td><a href="https://steemit.com/steemitboard/@steemitboard/use-your-witness-votes-and-get-the-community-badge"><img src="https://steemitimages.com/64x128/https://cdn.steemitimages.com/DQmTugCUsoXX762vg1CuHRrpnPbfnjPogp8iCGv7F2kSVuj/image.png"></a></td><td><a href="https://steemit.com/steemitboard/@steemitboard/use-your-witness-votes-and-get-the-community-badge">Use your witness votes and get the Community Badge</a></td></tr></table> ###### [Vote for @Steemitboard as a witness](https://v2.steemconnect.com/sign/account-witness-vote?witness=steemitboard&approve=1) to get one more award and increased upvotes!
json metadata{"image":["https://steemitboard.com/img/notify.png"]}
parent authordavidpm
parent permlinkdisabling-dangerous-redis-commands-in-production-redux
permlinksteemitboard-notify-davidpm-20200305t033430000z
title
Transaction InfoBlock #41375143/Trx 685cc2a172b99fd7ed31475ac98750b0a774cdf2
View Raw JSON Data
{
  "block": 41375143,
  "op": [
    "comment",
    {
      "author": "steemitboard",
      "body": "Congratulations @davidpm! You received a personal award!\n\n<table><tr><td>https://steemitimages.com/70x70/http://steemitboard.com/@davidpm/birthday2.png</td><td>Happy Birthday! - You are on the Steem blockchain for 2 years!</td></tr></table>\n\n<sub>_You can view [your badges on your Steem Board](https://steemitboard.com/@davidpm) and compare to others on the [Steem Ranking](https://steemitboard.com/ranking/index.php?name=davidpm)_</sub>\n\n\n**Do not miss the last post from @steemitboard:**\n<table><tr><td><a href=\"https://steemit.com/steemitboard/@steemitboard/use-your-witness-votes-and-get-the-community-badge\"><img src=\"https://steemitimages.com/64x128/https://cdn.steemitimages.com/DQmTugCUsoXX762vg1CuHRrpnPbfnjPogp8iCGv7F2kSVuj/image.png\"></a></td><td><a href=\"https://steemit.com/steemitboard/@steemitboard/use-your-witness-votes-and-get-the-community-badge\">Use your witness votes and get the Community Badge</a></td></tr></table>\n\n###### [Vote for @Steemitboard as a witness](https://v2.steemconnect.com/sign/account-witness-vote?witness=steemitboard&approve=1) to get one more award and increased upvotes!",
      "json_metadata": "{\"image\":[\"https://steemitboard.com/img/notify.png\"]}",
      "parent_author": "davidpm",
      "parent_permlink": "disabling-dangerous-redis-commands-in-production-redux",
      "permlink": "steemitboard-notify-davidpm-20200305t033430000z",
      "title": ""
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2020-03-05T03:34:30",
  "trx_id": "685cc2a172b99fd7ed31475ac98750b0a774cdf2",
  "trx_in_block": 10,
  "virtual_op": 0
}
steemdelegated 5.945 SP to @davidpm
2019/11/08 04:59:03
delegateedavidpm
delegatorsteem
vesting shares9664.078231 VESTS
Transaction InfoBlock #37985985/Trx 0870e92a6fc8a1380b34138978c97a9dead9f005
View Raw JSON Data
{
  "block": 37985985,
  "op": [
    "delegate_vesting_shares",
    {
      "delegatee": "davidpm",
      "delegator": "steem",
      "vesting_shares": "9664.078231 VESTS"
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2019-11-08T04:59:03",
  "trx_id": "0870e92a6fc8a1380b34138978c97a9dead9f005",
  "trx_in_block": 7,
  "virtual_op": 0
}
2019/08/18 19:38:12
authordavidpm
body@@ -2742,12 +2742,20 @@ ier -ya. +for ya (%E1%B5%94%E1%B4%A5%E1%B5%94) %0A%0ATh
json metadata{"tags":["programming","ruby","honeybadger","redis"],"links":["https://www.honeybadger.io/leveling-up/safeguarding-redis/","weedmaps.com","https://gist.github.com/joshuap/c1ff2657c150df6fb1257398b1d2716b","https://gist.github.com/david-pm/748846ba853d2b48834f0cca6fce2b6e"],"app":"steemit/0.1","format":"markdown","image":["https://dwglogo.com/wp-content/uploads/2017/12/1100px_Redis_Logo_01.png"]}
parent author
parent permlinkprogramming
permlinkdisabling-dangerous-redis-commands-in-production-redux
titleDisabling dangerous redis commands in Production redux
Transaction InfoBlock #35668553/Trx 1709f5657c42d1ad9145fc39979759c4022f50a3
View Raw JSON Data
{
  "block": 35668553,
  "op": [
    "comment",
    {
      "author": "davidpm",
      "body": "@@ -2742,12 +2742,20 @@\n ier \n-ya. \n+for ya (%E1%B5%94%E1%B4%A5%E1%B5%94)\n %0A%0ATh\n",
      "json_metadata": "{\"tags\":[\"programming\",\"ruby\",\"honeybadger\",\"redis\"],\"links\":[\"https://www.honeybadger.io/leveling-up/safeguarding-redis/\",\"weedmaps.com\",\"https://gist.github.com/joshuap/c1ff2657c150df6fb1257398b1d2716b\",\"https://gist.github.com/david-pm/748846ba853d2b48834f0cca6fce2b6e\"],\"app\":\"steemit/0.1\",\"format\":\"markdown\",\"image\":[\"https://dwglogo.com/wp-content/uploads/2017/12/1100px_Redis_Logo_01.png\"]}",
      "parent_author": "",
      "parent_permlink": "programming",
      "permlink": "disabling-dangerous-redis-commands-in-production-redux",
      "title": "Disabling dangerous redis commands in Production redux"
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2019-08-18T19:38:12",
  "trx_id": "1709f5657c42d1ad9145fc39979759c4022f50a3",
  "trx_in_block": 2,
  "virtual_op": 0
}
steemdelegated 18.186 SP to @davidpm
2019/08/09 04:18:03
delegateedavidpm
delegatorsteem
vesting shares29563.286980 VESTS
Transaction InfoBlock #35391454/Trx d3042cdd0590d2d4b5307d855aee92e1cc419493
View Raw JSON Data
{
  "block": 35391454,
  "op": [
    "delegate_vesting_shares",
    {
      "delegatee": "davidpm",
      "delegator": "steem",
      "vesting_shares": "29563.286980 VESTS"
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2019-08-09T04:18:03",
  "trx_id": "d3042cdd0590d2d4b5307d855aee92e1cc419493",
  "trx_in_block": 6,
  "virtual_op": 0
}
resteemyousent 0.001 STEEM to @davidpm- "Hi! I re-blog posts to 7000+ followers if you send only 0.03 SBD/Steem with post link in memo || comments disabled. Thanx ♥"
2019/08/09 03:12:21
amount0.001 STEEM
fromresteemyou
memoHi! I re-blog posts to 7000+ followers if you send only 0.03 SBD/Steem with post link in memo || comments disabled. Thanx ♥
todavidpm
Transaction InfoBlock #35390144/Trx 0d36a0f746974927b13dff844eae370e564f49c4
View Raw JSON Data
{
  "block": 35390144,
  "op": [
    "transfer",
    {
      "amount": "0.001 STEEM",
      "from": "resteemyou",
      "memo": "Hi! I re-blog posts to 7000+ followers if you send  only 0.03 SBD/Steem with post link in memo || comments disabled. Thanx ♥",
      "to": "davidpm"
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2019-08-09T03:12:21",
  "trx_id": "0d36a0f746974927b13dff844eae370e564f49c4",
  "trx_in_block": 16,
  "virtual_op": 0
}
2019/08/09 03:03:48
authordavidpm
body@@ -1,8 +1,90 @@ +!%5Bredis%5D(https://dwglogo.com/wp-content/uploads/2017/12/1100px_Redis_Logo_01.png)%0A I recent
json metadata{"tags":["programming","ruby","honeybadger","redis"],"links":["https://www.honeybadger.io/leveling-up/safeguarding-redis/","weedmaps.com","https://gist.github.com/joshuap/c1ff2657c150df6fb1257398b1d2716b","https://gist.github.com/david-pm/748846ba853d2b48834f0cca6fce2b6e"],"app":"steemit/0.1","format":"markdown","image":["https://dwglogo.com/wp-content/uploads/2017/12/1100px_Redis_Logo_01.png"]}
parent author
parent permlinkprogramming
permlinkdisabling-dangerous-redis-commands-in-production-redux
titleDisabling dangerous redis commands in Production redux
Transaction InfoBlock #35389973/Trx 94f424937915d70b90e20276095249ab3f25c8d6
View Raw JSON Data
{
  "block": 35389973,
  "op": [
    "comment",
    {
      "author": "davidpm",
      "body": "@@ -1,8 +1,90 @@\n+!%5Bredis%5D(https://dwglogo.com/wp-content/uploads/2017/12/1100px_Redis_Logo_01.png)%0A\n I recent\n",
      "json_metadata": "{\"tags\":[\"programming\",\"ruby\",\"honeybadger\",\"redis\"],\"links\":[\"https://www.honeybadger.io/leveling-up/safeguarding-redis/\",\"weedmaps.com\",\"https://gist.github.com/joshuap/c1ff2657c150df6fb1257398b1d2716b\",\"https://gist.github.com/david-pm/748846ba853d2b48834f0cca6fce2b6e\"],\"app\":\"steemit/0.1\",\"format\":\"markdown\",\"image\":[\"https://dwglogo.com/wp-content/uploads/2017/12/1100px_Redis_Logo_01.png\"]}",
      "parent_author": "",
      "parent_permlink": "programming",
      "permlink": "disabling-dangerous-redis-commands-in-production-redux",
      "title": "Disabling dangerous redis commands in Production redux"
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2019-08-09T03:03:48",
  "trx_id": "94f424937915d70b90e20276095249ab3f25c8d6",
  "trx_in_block": 9,
  "virtual_op": 0
}
2019/08/09 03:02:03
authordavidpm
permlinkdisabling-dangerous-redis-commands-in-production-redux
votercron
weight2 (0.02%)
Transaction InfoBlock #35389938/Trx 35af34476e6a92f0fe17d03bb9fe461a8facde18
View Raw JSON Data
{
  "block": 35389938,
  "op": [
    "vote",
    {
      "author": "davidpm",
      "permlink": "disabling-dangerous-redis-commands-in-production-redux",
      "voter": "cron",
      "weight": 2
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2019-08-09T03:02:03",
  "trx_id": "35af34476e6a92f0fe17d03bb9fe461a8facde18",
  "trx_in_block": 27,
  "virtual_op": 0
}
2019/08/09 03:01:57
authordavidpm
bodyI recently read a great article from [Honeybadger](https://www.honeybadger.io/leveling-up/safeguarding-redis/) that muses on what could happen to a production environment if a fat fingered developer did something like: `Redis.current.flushdb`. This scared me enough that I decided we needed something like this at [Weedmaps](weedmaps.com). The only problem is that we want to keep these commands available in lower environments. We also do not allow new code into our codebases without tests. We make exceptions but they are rare, since we use our tests as documentation and a sanity check. Here is my adaptation to this [gist]https://gist.github.com/joshuap/c1ff2657c150df6fb1257398b1d2716b) which was provided in the Honeybadger article above: ``` ############### config/initializers/redis.rb ######################### require 'redis' require 'redis_dangerous_commands' class Redis prepend DangerousCommands end ############### lib/redis_dangerous_commands.rb ######################### module DangerousCommands class ProhibitedCommandError < StandardError; end def self.prepended(base) return unless Rails.env.production? base.send(:define_method, :flushdb) do raise ProhibitedCommandError, error_message('EMPTY THE ENTIRE DATABASE') end base.send(:define_method, :flushall) do raise ProhibitedCommandError, error_message('FLUSH ALL DATABASES') end end def error_message(inner_message) "DANGEROUS destructive operation. If you really want to #{inner_message} do it from `redis-cli`." end end ############### spec/lib/redis_dangerous_commands_spec.rb ######################### describe 'DangerousCommands' do context 'Production' do let(:dumb) { Dummy.new } before do allow(Rails).to receive(:env).and_return(ActiveSupport::StringInquirer.new('production')) class Dummy prepend DangerousCommands end end it 'raises ProhibitedCommandError' do expect{ dumb.flushdb }.to raise_error DangerousCommands::ProhibitedCommandError end it 'raises ProhibitedCommandError' do expect{ dumb.flushall }.to raise_error DangerousCommands::ProhibitedCommandError end end context 'Non-production' do let(:dumb) { DifferentDummy.new } before do class DifferentDummy prepend DangerousCommands end end it 'does not respond to flushdb' do expect(dumb).not_to respond_to(:flushdb) end it 'does not respond to flushall' do expect(dumb).not_to respond_to(:flushall) end end end ``` And a link to a [gist](https://gist.github.com/david-pm/748846ba853d2b48834f0cca6fce2b6e) if that's easier ya. The basic idea is that we needed a way to a) optionally include the disabled override methods based on the `Rails.env` and b) test this. The trick was figuring out how to stub `Rails.env` in an initializer spec. Turns out it isn't possible since the environment loads those initializers as it boots up to run your specs. By time you stub out your `Rails.env` it is too late. This approach only defines those methods in the prepended hook provided by Ruby. Since, this module lives in `lib` we can stub our `Rails.env` and prepend our module into some dummy classes. If the dummy class receives those methods in when we stub out a production env then we know we are in business. ~(˘▾˘~)
json metadata{"tags":["programming","ruby","honeybadger","redis"],"links":["https://www.honeybadger.io/leveling-up/safeguarding-redis/","weedmaps.com","https://gist.github.com/joshuap/c1ff2657c150df6fb1257398b1d2716b","https://gist.github.com/david-pm/748846ba853d2b48834f0cca6fce2b6e"],"app":"steemit/0.1","format":"markdown"}
parent author
parent permlinkprogramming
permlinkdisabling-dangerous-redis-commands-in-production-redux
titleDisabling dangerous redis commands in Production redux
Transaction InfoBlock #35389936/Trx 4b2a93bd0eb760c2b5e41491dc7b1a2b1db28fd0
View Raw JSON Data
{
  "block": 35389936,
  "op": [
    "comment",
    {
      "author": "davidpm",
      "body": "I recently read a great article from [Honeybadger](https://www.honeybadger.io/leveling-up/safeguarding-redis/) that muses on what could happen to a production environment if a fat fingered developer did something like:\n`Redis.current.flushdb`.\n\nThis scared me enough that I decided we needed something like this at [Weedmaps](weedmaps.com). The only problem is that we want to keep these commands available in lower environments. We also do not allow new code into our codebases without tests. We make exceptions but they are rare, since we use our tests as documentation and a sanity check.\n\nHere is my adaptation to this [gist]https://gist.github.com/joshuap/c1ff2657c150df6fb1257398b1d2716b) which was provided in the Honeybadger article above:\n```\n############### config/initializers/redis.rb #########################\nrequire 'redis'\nrequire 'redis_dangerous_commands'\n\nclass Redis\n  prepend DangerousCommands\nend\n\n############### lib/redis_dangerous_commands.rb #########################\nmodule DangerousCommands\n  class ProhibitedCommandError < StandardError; end\n\n  def self.prepended(base)\n    return unless Rails.env.production?\n\n    base.send(:define_method, :flushdb) do\n      raise ProhibitedCommandError, error_message('EMPTY THE ENTIRE DATABASE')\n    end\n\n    base.send(:define_method, :flushall) do\n      raise ProhibitedCommandError, error_message('FLUSH ALL DATABASES')\n    end\n  end\n\n  def error_message(inner_message)\n    \"DANGEROUS destructive operation. If you really want to #{inner_message} do it from `redis-cli`.\"\n  end\nend\n\n############### spec/lib/redis_dangerous_commands_spec.rb #########################\ndescribe 'DangerousCommands' do\n  context 'Production' do\n    let(:dumb) { Dummy.new }\n\n    before do\n      allow(Rails).to receive(:env).and_return(ActiveSupport::StringInquirer.new('production'))\n      class Dummy\n        prepend DangerousCommands\n      end\n    end\n\n    it 'raises ProhibitedCommandError' do\n      expect{ dumb.flushdb }.to raise_error DangerousCommands::ProhibitedCommandError\n    end\n\n    it 'raises ProhibitedCommandError' do\n      expect{ dumb.flushall }.to raise_error DangerousCommands::ProhibitedCommandError\n    end\n  end\n\n  context 'Non-production' do\n    let(:dumb) { DifferentDummy.new }\n\n    before do\n      class DifferentDummy\n        prepend DangerousCommands\n      end\n    end\n\n    it 'does not respond to flushdb' do\n      expect(dumb).not_to respond_to(:flushdb)\n    end\n\n    it 'does not respond to flushall' do\n      expect(dumb).not_to respond_to(:flushall)\n    end\n  end\nend\n```\n\nAnd a link to a [gist](https://gist.github.com/david-pm/748846ba853d2b48834f0cca6fce2b6e) if that's easier ya. \n\nThe basic idea is that we needed a way to a) optionally include the disabled override methods based on the `Rails.env`\nand b) test this. The trick was figuring out how to stub `Rails.env` in an initializer spec. Turns out it isn't possible since the environment loads those initializers as it boots up to run your specs. By time you stub out your `Rails.env` it is too late.\n\nThis approach only defines those methods in the prepended hook provided by Ruby. Since, this module lives in `lib`\nwe can stub our `Rails.env` and prepend our module into some dummy classes. If the dummy class receives those methods in when we stub out a production env then we know we are in business.\n\n~(˘▾˘~)",
      "json_metadata": "{\"tags\":[\"programming\",\"ruby\",\"honeybadger\",\"redis\"],\"links\":[\"https://www.honeybadger.io/leveling-up/safeguarding-redis/\",\"weedmaps.com\",\"https://gist.github.com/joshuap/c1ff2657c150df6fb1257398b1d2716b\",\"https://gist.github.com/david-pm/748846ba853d2b48834f0cca6fce2b6e\"],\"app\":\"steemit/0.1\",\"format\":\"markdown\"}",
      "parent_author": "",
      "parent_permlink": "programming",
      "permlink": "disabling-dangerous-redis-commands-in-production-redux",
      "title": "Disabling dangerous redis commands in Production redux"
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2019-08-09T03:01:57",
  "trx_id": "4b2a93bd0eb760c2b5e41491dc7b1a2b1db28fd0",
  "trx_in_block": 14,
  "virtual_op": 0
}
2019/03/03 04:21:03
authorsteemitboard
bodyCongratulations @davidpm! You received a personal award! <table><tr><td>https://steemitimages.com/70x70/http://steemitboard.com/@davidpm/birthday1.png</td><td>Happy Birthday! - You are on the Steem blockchain for 1 year!</td></tr></table> <sub>_[Click here to view your Board](https://steemitboard.com/@davidpm)_</sub> **Do not miss the last post from @steemitboard:** <table><tr><td><a href="https://steemit.com/carnival/@steemitboard/carnival-2019"><img src="https://steemitimages.com/64x128/http://i.cubeupload.com/rltzHT.png"></a></td><td><a href="https://steemit.com/carnival/@steemitboard/carnival-2019">Carnival Challenge - Collect badge and win 5 STEEM</a></td></tr></table> ###### [Vote for @Steemitboard as a witness](https://v2.steemconnect.com/sign/account-witness-vote?witness=steemitboard&approve=1) and get one more award and increased upvotes!
json metadata{"image":["https://steemitboard.com/img/notify.png"]}
parent authordavidpm
parent permlinkheuristics-on-the-practice-of-programming
permlinksteemitboard-notify-davidpm-20190303t042102000z
title
Transaction InfoBlock #30819645/Trx 739326d8102d8f2f17661db4929f57d0a8fa0350
View Raw JSON Data
{
  "block": 30819645,
  "op": [
    "comment",
    {
      "author": "steemitboard",
      "body": "Congratulations @davidpm! You received a personal award!\n\n<table><tr><td>https://steemitimages.com/70x70/http://steemitboard.com/@davidpm/birthday1.png</td><td>Happy Birthday! - You are on the Steem blockchain for 1 year!</td></tr></table>\n\n<sub>_[Click here to view your Board](https://steemitboard.com/@davidpm)_</sub>\n\n\n**Do not miss the last post from @steemitboard:**\n<table><tr><td><a href=\"https://steemit.com/carnival/@steemitboard/carnival-2019\"><img src=\"https://steemitimages.com/64x128/http://i.cubeupload.com/rltzHT.png\"></a></td><td><a href=\"https://steemit.com/carnival/@steemitboard/carnival-2019\">Carnival Challenge - Collect badge and win 5 STEEM</a></td></tr></table>\n\n###### [Vote for @Steemitboard as a witness](https://v2.steemconnect.com/sign/account-witness-vote?witness=steemitboard&approve=1) and get one more award and increased upvotes!",
      "json_metadata": "{\"image\":[\"https://steemitboard.com/img/notify.png\"]}",
      "parent_author": "davidpm",
      "parent_permlink": "heuristics-on-the-practice-of-programming",
      "permlink": "steemitboard-notify-davidpm-20190303t042102000z",
      "title": ""
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2019-03-03T04:21:03",
  "trx_id": "739326d8102d8f2f17661db4929f57d0a8fa0350",
  "trx_in_block": 3,
  "virtual_op": 0
}
2019/02/03 07:48:00
authordavidpm
body@@ -2708,20 +2708,16 @@ u could -all install
json metadata{"tags":["ruby","gem","thor","cli","matrix"],"format":"markdown","links":["https://steemit.com/ruby/@davidpm/create-a-command-line-gem-from-scratch-with-thor-part-two","https://bundler.io/","https://git-scm.com/","http://rspec.info/","https://github.com/ruby/rake","https://github.com/erikhuda/thor","https://github.com/bundler/bundler/blob/master/lib/bundler/dsl.rb#L58","https://en.wikipedia.org/wiki/Ouroboros"],"image":["https://media.giphy.com/media/S4HKH9KgRGMdq/giphy.gif","https://upload.wikimedia.org/wikipedia/commons/3/32/Chrysopoea_of_Cleopatra_1.png"],"app":"steemit/0.1"}
parent author
parent permlinkruby
permlinkcreate-a-command-line-gem-from-scratch-with-thor
titleCreate a command-line gem from scratch with Thor
Transaction InfoBlock #30018006/Trx 33b44980ba5712c327431816c24720248aa0af70
View Raw JSON Data
{
  "block": 30018006,
  "op": [
    "comment",
    {
      "author": "davidpm",
      "body": "@@ -2708,20 +2708,16 @@\n u could \n-all \n install \n",
      "json_metadata": "{\"tags\":[\"ruby\",\"gem\",\"thor\",\"cli\",\"matrix\"],\"format\":\"markdown\",\"links\":[\"https://steemit.com/ruby/@davidpm/create-a-command-line-gem-from-scratch-with-thor-part-two\",\"https://bundler.io/\",\"https://git-scm.com/\",\"http://rspec.info/\",\"https://github.com/ruby/rake\",\"https://github.com/erikhuda/thor\",\"https://github.com/bundler/bundler/blob/master/lib/bundler/dsl.rb#L58\",\"https://en.wikipedia.org/wiki/Ouroboros\"],\"image\":[\"https://media.giphy.com/media/S4HKH9KgRGMdq/giphy.gif\",\"https://upload.wikimedia.org/wikipedia/commons/3/32/Chrysopoea_of_Cleopatra_1.png\"],\"app\":\"steemit/0.1\"}",
      "parent_author": "",
      "parent_permlink": "ruby",
      "permlink": "create-a-command-line-gem-from-scratch-with-thor",
      "title": "Create a command-line gem from scratch with Thor"
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2019-02-03T07:48:00",
  "trx_id": "33b44980ba5712c327431816c24720248aa0af70",
  "trx_in_block": 11,
  "virtual_op": 0
}
2018/12/27 13:19:09
authordavidpm
permlinkheuristics-on-the-practice-of-programming
voterjohnteee
weight10000 (100.00%)
Transaction InfoBlock #28931242/Trx bf68582bdc4744a81793619cc57d0547de12a537
View Raw JSON Data
{
  "block": 28931242,
  "op": [
    "vote",
    {
      "author": "davidpm",
      "permlink": "heuristics-on-the-practice-of-programming",
      "voter": "johnteee",
      "weight": 10000
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2018-12-27T13:19:09",
  "trx_id": "bf68582bdc4744a81793619cc57d0547de12a537",
  "trx_in_block": 51,
  "virtual_op": 0
}
2018/12/16 23:17:45
authordavidpm
body@@ -908,16 +908,127 @@ e bug.%0A%0A +* %5BFunctions should do one thing%5D(https://github.com/uohzxela/clean-code-ruby#functions-should-do-one-thing).%0A%0A * Everyt
json metadata{"tags":["technology","programming"],"app":"steemit/0.1","format":"markdown","image":["https://cdn.steemitimages.com/DQmbN4HMppNXVRkvfxQYXhev3KYHdAMfFCnbBSnfBPkA5v7/Screen%20Shot%202018-09-04%20at%202.46.01%20PM.png"],"links":["https://github.com/uohzxela/clean-code-ruby#functions-should-do-one-thing"]}
parent author
parent permlinktechnology
permlinkheuristics-on-the-practice-of-programming
titleHeuristics on the Practice of Programming
Transaction InfoBlock #28626596/Trx fecbcd0896c8bd02dfd4954e6e550ab8bcd58ae3
View Raw JSON Data
{
  "block": 28626596,
  "op": [
    "comment",
    {
      "author": "davidpm",
      "body": "@@ -908,16 +908,127 @@\n e bug.%0A%0A\n+* %5BFunctions should do one thing%5D(https://github.com/uohzxela/clean-code-ruby#functions-should-do-one-thing).%0A%0A\n * Everyt\n",
      "json_metadata": "{\"tags\":[\"technology\",\"programming\"],\"app\":\"steemit/0.1\",\"format\":\"markdown\",\"image\":[\"https://cdn.steemitimages.com/DQmbN4HMppNXVRkvfxQYXhev3KYHdAMfFCnbBSnfBPkA5v7/Screen%20Shot%202018-09-04%20at%202.46.01%20PM.png\"],\"links\":[\"https://github.com/uohzxela/clean-code-ruby#functions-should-do-one-thing\"]}",
      "parent_author": "",
      "parent_permlink": "technology",
      "permlink": "heuristics-on-the-practice-of-programming",
      "title": "Heuristics on the Practice of Programming"
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2018-12-16T23:17:45",
  "trx_id": "fecbcd0896c8bd02dfd4954e6e550ab8bcd58ae3",
  "trx_in_block": 5,
  "virtual_op": 0
}
steemdelegated 6.066 SP to @davidpm
2018/12/04 22:40:06
delegateedavidpm
delegatorsteem
vesting shares9860.997057 VESTS
Transaction InfoBlock #28280460/Trx 82be5210f09e2d4db69000ef14f1180e3c14a810
View Raw JSON Data
{
  "block": 28280460,
  "op": [
    "delegate_vesting_shares",
    {
      "delegatee": "davidpm",
      "delegator": "steem",
      "vesting_shares": "9860.997057 VESTS"
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2018-12-04T22:40:06",
  "trx_id": "82be5210f09e2d4db69000ef14f1180e3c14a810",
  "trx_in_block": 2,
  "virtual_op": 0
}
steemdelegated 18.458 SP to @davidpm
2018/11/26 17:11:36
delegateedavidpm
delegatorsteem
vesting shares30005.480771 VESTS
Transaction InfoBlock #28043573/Trx 8126a36cd7bd7070f952354e7ebf9883cfd7da69
View Raw JSON Data
{
  "block": 28043573,
  "op": [
    "delegate_vesting_shares",
    {
      "delegatee": "davidpm",
      "delegator": "steem",
      "vesting_shares": "30005.480771 VESTS"
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2018-11-26T17:11:36",
  "trx_id": "8126a36cd7bd7070f952354e7ebf9883cfd7da69",
  "trx_in_block": 38,
  "virtual_op": 0
}
davidpmreceived 0.033 STEEM, 0.042 SP author reward for @davidpm / heuristics-on-the-practice-of-programming
2018/09/11 21:30:27
authordavidpm
permlinkheuristics-on-the-practice-of-programming
sbd payout0.000 SBD
steem payout0.033 STEEM
vesting payout68.755111 VESTS
Transaction InfoBlock #25877154/Virtual Operation #4
View Raw JSON Data
{
  "block": 25877154,
  "op": [
    "author_reward",
    {
      "author": "davidpm",
      "permlink": "heuristics-on-the-practice-of-programming",
      "sbd_payout": "0.000 SBD",
      "steem_payout": "0.033 STEEM",
      "vesting_payout": "68.755111 VESTS"
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2018-09-11T21:30:27",
  "trx_id": "0000000000000000000000000000000000000000",
  "trx_in_block": 4294967295,
  "virtual_op": 4
}
2018/09/05 03:23:15
authordavidpm
permlinkheuristics-on-the-practice-of-programming
votermsicc
weight10000 (100.00%)
Transaction InfoBlock #25682677/Trx e23140a46a4f66e1173c111bedd0365869a1ce8e
View Raw JSON Data
{
  "block": 25682677,
  "op": [
    "vote",
    {
      "author": "davidpm",
      "permlink": "heuristics-on-the-practice-of-programming",
      "voter": "msicc",
      "weight": 10000
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2018-09-05T03:23:15",
  "trx_id": "e23140a46a4f66e1173c111bedd0365869a1ce8e",
  "trx_in_block": 31,
  "virtual_op": 0
}
2018/09/04 22:00:33
authordavidpm
permlinkheuristics-on-the-practice-of-programming
voterhr1
weight2 (0.02%)
Transaction InfoBlock #25676234/Trx 3cc03ede6703a8d4f9977283a0b13dca5d0c2414
View Raw JSON Data
{
  "block": 25676234,
  "op": [
    "vote",
    {
      "author": "davidpm",
      "permlink": "heuristics-on-the-practice-of-programming",
      "voter": "hr1",
      "weight": 2
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2018-09-04T22:00:33",
  "trx_id": "3cc03ede6703a8d4f9977283a0b13dca5d0c2414",
  "trx_in_block": 17,
  "virtual_op": 0
}
2018/09/04 21:51:36
authordavidpm
permlinkheuristics-on-the-practice-of-programming
voteralphabot
weight100 (1.00%)
Transaction InfoBlock #25676055/Trx 585efafaf1e7605678620e3d5bd27671f6eedf08
View Raw JSON Data
{
  "block": 25676055,
  "op": [
    "vote",
    {
      "author": "davidpm",
      "permlink": "heuristics-on-the-practice-of-programming",
      "voter": "alphabot",
      "weight": 100
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2018-09-04T21:51:36",
  "trx_id": "585efafaf1e7605678620e3d5bd27671f6eedf08",
  "trx_in_block": 4,
  "virtual_op": 0
}
2018/09/04 21:51:12
authordavidpm
body@@ -1969,16 +1969,17 @@ her.%0A%0A* +%22 Just bec @@ -2041,16 +2041,17 @@ t a bug. +%22 -Piers
json metadata{"tags":["technology","programming"],"app":"steemit/0.1","format":"markdown","image":["https://cdn.steemitimages.com/DQmbN4HMppNXVRkvfxQYXhev3KYHdAMfFCnbBSnfBPkA5v7/Screen%20Shot%202018-09-04%20at%202.46.01%20PM.png"]}
parent author
parent permlinktechnology
permlinkheuristics-on-the-practice-of-programming
titleHeuristics on the Practice of Programming
Transaction InfoBlock #25676047/Trx b86084588012b72002fc979fbe7179aecb5e530a
View Raw JSON Data
{
  "block": 25676047,
  "op": [
    "comment",
    {
      "author": "davidpm",
      "body": "@@ -1969,16 +1969,17 @@\n her.%0A%0A* \n+%22\n Just bec\n@@ -2041,16 +2041,17 @@\n t a bug.\n+%22\n  -Piers \n",
      "json_metadata": "{\"tags\":[\"technology\",\"programming\"],\"app\":\"steemit/0.1\",\"format\":\"markdown\",\"image\":[\"https://cdn.steemitimages.com/DQmbN4HMppNXVRkvfxQYXhev3KYHdAMfFCnbBSnfBPkA5v7/Screen%20Shot%202018-09-04%20at%202.46.01%20PM.png\"]}",
      "parent_author": "",
      "parent_permlink": "technology",
      "permlink": "heuristics-on-the-practice-of-programming",
      "title": "Heuristics on the Practice of Programming"
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2018-09-04T21:51:12",
  "trx_id": "b86084588012b72002fc979fbe7179aecb5e530a",
  "trx_in_block": 16,
  "virtual_op": 0
}
2018/09/04 21:47:33
authordavidpm
permlinkheuristics-on-the-practice-of-programming
voterfastresteem
weight100 (1.00%)
Transaction InfoBlock #25675974/Trx 66bcd58e6999bdcb5c814f165b78a907b4ae8c93
View Raw JSON Data
{
  "block": 25675974,
  "op": [
    "vote",
    {
      "author": "davidpm",
      "permlink": "heuristics-on-the-practice-of-programming",
      "voter": "fastresteem",
      "weight": 100
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2018-09-04T21:47:33",
  "trx_id": "66bcd58e6999bdcb5c814f165b78a907b4ae8c93",
  "trx_in_block": 0,
  "virtual_op": 0
}
2018/09/04 21:47:18
authordavidpm
body@@ -1,12 +1,186 @@ +!%5BScreen Shot 2018-09-04 at 2.46.01 PM.png%5D(https://cdn.steemitimages.com/DQmbN4HMppNXVRkvfxQYXhev3KYHdAMfFCnbBSnfBPkA5v7/Screen%2520Shot%25202018-09-04%2520at%25202.46.01%2520PM.png)%0A ### A Living
json metadata{"tags":["technology","programming"],"app":"steemit/0.1","format":"markdown","image":["https://cdn.steemitimages.com/DQmbN4HMppNXVRkvfxQYXhev3KYHdAMfFCnbBSnfBPkA5v7/Screen%20Shot%202018-09-04%20at%202.46.01%20PM.png"]}
parent author
parent permlinktechnology
permlinkheuristics-on-the-practice-of-programming
titleHeuristics on the Practice of Programming
Transaction InfoBlock #25675969/Trx cc0d6277e94225371a9e8030610c2359cc8e213b
View Raw JSON Data
{
  "block": 25675969,
  "op": [
    "comment",
    {
      "author": "davidpm",
      "body": "@@ -1,12 +1,186 @@\n+!%5BScreen Shot 2018-09-04 at 2.46.01 PM.png%5D(https://cdn.steemitimages.com/DQmbN4HMppNXVRkvfxQYXhev3KYHdAMfFCnbBSnfBPkA5v7/Screen%2520Shot%25202018-09-04%2520at%25202.46.01%2520PM.png)%0A\n ### A Living\n",
      "json_metadata": "{\"tags\":[\"technology\",\"programming\"],\"app\":\"steemit/0.1\",\"format\":\"markdown\",\"image\":[\"https://cdn.steemitimages.com/DQmbN4HMppNXVRkvfxQYXhev3KYHdAMfFCnbBSnfBPkA5v7/Screen%20Shot%202018-09-04%20at%202.46.01%20PM.png\"]}",
      "parent_author": "",
      "parent_permlink": "technology",
      "permlink": "heuristics-on-the-practice-of-programming",
      "title": "Heuristics on the Practice of Programming"
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2018-09-04T21:47:18",
  "trx_id": "cc0d6277e94225371a9e8030610c2359cc8e213b",
  "trx_in_block": 19,
  "virtual_op": 0
}
2018/09/04 21:31:30
idfollow
json["follow",{"follower":"davidpm","following":"ilovecoding","what":["blog"]}]
required auths[]
required posting auths["davidpm"]
Transaction InfoBlock #25675653/Trx 621bf2dbbcda49b97fca67dabf29094d82d7c400
View Raw JSON Data
{
  "block": 25675653,
  "op": [
    "custom_json",
    {
      "id": "follow",
      "json": "[\"follow\",{\"follower\":\"davidpm\",\"following\":\"ilovecoding\",\"what\":[\"blog\"]}]",
      "required_auths": [],
      "required_posting_auths": [
        "davidpm"
      ]
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2018-09-04T21:31:30",
  "trx_id": "621bf2dbbcda49b97fca67dabf29094d82d7c400",
  "trx_in_block": 34,
  "virtual_op": 0
}
2018/09/04 21:30:39
authorilovecoding
bodyHello! Your post has been resteemed and upvoted by @ilovecoding because **we love coding**! Keep up good work! Consider upvoting this comment to support the @ilovecoding and increase your future rewards! ^_^ Steem On! ![](https://codingforspeed.com/images/i-love-coding.jpg) *Reply !stop to disable the comment. Thanks!*
json metadata{"tags":["ilovecoding"],"app":"ilovecoding"}
parent authordavidpm
parent permlinkheuristics-on-the-practice-of-programming
permlink20180904t213039359z
title
Transaction InfoBlock #25675636/Trx ea782363c789158f241ad756ac85987db0ec0dfb
View Raw JSON Data
{
  "block": 25675636,
  "op": [
    "comment",
    {
      "author": "ilovecoding",
      "body": "Hello! Your post has been resteemed and upvoted by @ilovecoding because **we love coding**! Keep up good work! Consider upvoting this comment to support the @ilovecoding and increase your future rewards! ^_^ Steem On! \n ![](https://codingforspeed.com/images/i-love-coding.jpg) \n*Reply !stop to disable the comment. Thanks!*",
      "json_metadata": "{\"tags\":[\"ilovecoding\"],\"app\":\"ilovecoding\"}",
      "parent_author": "davidpm",
      "parent_permlink": "heuristics-on-the-practice-of-programming",
      "permlink": "20180904t213039359z",
      "title": ""
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2018-09-04T21:30:39",
  "trx_id": "ea782363c789158f241ad756ac85987db0ec0dfb",
  "trx_in_block": 30,
  "virtual_op": 0
}
2018/09/04 21:30:36
authordavidpm
permlinkheuristics-on-the-practice-of-programming
voterax3
weight100 (1.00%)
Transaction InfoBlock #25675635/Trx 73423f15dcf0572c77f3023f87ebcfa0a630449b
View Raw JSON Data
{
  "block": 25675635,
  "op": [
    "vote",
    {
      "author": "davidpm",
      "permlink": "heuristics-on-the-practice-of-programming",
      "voter": "ax3",
      "weight": 100
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2018-09-04T21:30:36",
  "trx_id": "73423f15dcf0572c77f3023f87ebcfa0a630449b",
  "trx_in_block": 22,
  "virtual_op": 0
}
2018/09/04 21:30:36
authordavidpm
permlinkheuristics-on-the-practice-of-programming
voterilovecoding
weight1000 (10.00%)
Transaction InfoBlock #25675635/Trx ca0d844a533b7a5e4a201984b802800e750801a5
View Raw JSON Data
{
  "block": 25675635,
  "op": [
    "vote",
    {
      "author": "davidpm",
      "permlink": "heuristics-on-the-practice-of-programming",
      "voter": "ilovecoding",
      "weight": 1000
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2018-09-04T21:30:36",
  "trx_id": "ca0d844a533b7a5e4a201984b802800e750801a5",
  "trx_in_block": 15,
  "virtual_op": 0
}
2018/09/04 21:30:27
authordavidpm
body### A Living List What follows is a living list of heuristics I've come to internalize over the years as a developer. My goal is to periodically add to this list. Consider it a repository of programming wisdom. If you have something you'd like added to the list, feel free to comment and I may update the list ʕ•ᴥ•ʔ If you disagree with anything on the list, let's fight it out in the comments. I believe in respectful, healthy debate on the open marketplace of ideas. If you find any of these nuggets to be a paraphrase or quote from someone of note, let me know! I'm not interested in plagiarizing, just sharing. ### Sagesse * No matter how many alerts or people are screaming at you, the first step is to reliably replicate the bug. * Everything in software development has already been invented. * Don’t trust the compiler. Don’t trust the tools. Don’t trust the documentation. Don’t trust yourself. * We don’t need any more computer languages. * Maintaining code is harder than writing it. * Do not overload your working memory. Protect it, keep it focused only on the task at hand if possible. * You have been taught to program as though memory, processor time, and network bandwidth are all free and infinite: none is. * "We should forget about small efficiencies, say about 97% of the time: premature optimization is the root of all evil. Yet we should not pass up our opportunities in that critical 3%." -Knuth * You’re going to forget what your code does in a few months: make it easy to read. * Sometimes, all you need is a one-liner in `sed` * Beware of programmers who speak in absolutes. Programming is an art, not a religion. * If you know you will do a fixed sequence of steps more than ten times, automate it. * Backing it up is one thing. Restoring it is another. * Just because it works on your machine does not mean there is not a bug. -Piers Sutton * Wait for the point-one release of development tools before installing them. Let other people be guinea pigs. * Good programmers write good code. Great programmers write no code. Zen programmers delete code. * Lock in on one task and focus on completing it quickly and correctly, to the exclusion of all others. Every successful person does this. * Don't memorize commands or syntax; memorize concepts. Concentrate on problem solving. * When really stuck, go beyond questioning the code: question your assumptions about what the code does. * When you get overwhelmed by using new software just remember that you simply have to learn its API. Understand what it does, then learn its interface. * Ask yourself how hard your code will be to change later. Remember code is cheap. * "After you have left, remember to look back into the abyss." - Douglas Crockford * Almost everything `rms` has ever said re: proprietary software. ### (▀̿Ĺ̯▀̿ ̿)
json metadata{"tags":["technology","programming"],"app":"steemit/0.1","format":"markdown"}
parent author
parent permlinktechnology
permlinkheuristics-on-the-practice-of-programming
titleHeuristics on the Practice of Programming
Transaction InfoBlock #25675632/Trx c64e0c9b15136348dfec2ef6abdabe06794d91bf
View Raw JSON Data
{
  "block": 25675632,
  "op": [
    "comment",
    {
      "author": "davidpm",
      "body": "### A Living List\n\nWhat follows is a living list of heuristics I've come to internalize over the years as a developer. My goal is to periodically add to this list. Consider it a repository of programming wisdom. If you have something you'd like added to the list, feel free to comment and I may update the list ʕ•ᴥ•ʔ If you disagree with anything on the list, let's fight it out in the comments. I believe in respectful, healthy debate on the open marketplace of ideas. If you find any of these nuggets to be a paraphrase or quote from someone of note, let me know! I'm not interested in plagiarizing, just sharing. \n\n### Sagesse\n\n* No matter how many alerts or people are screaming at you, the first step is to reliably replicate the bug.\n\n* Everything in software development has already been invented.\n\n* Don’t trust the compiler. Don’t trust the tools. Don’t trust the documentation. Don’t trust yourself.\n\n* We don’t need any more computer languages.\n\n* Maintaining code is harder than writing it.\n\n* Do not overload your working memory. Protect it, keep it focused only on the task at hand if possible.\n\n* You have been taught to program as though memory, processor time, and network bandwidth are all free and infinite: none is. \n\n* \"We should forget about small efficiencies, say about 97% of the time: premature optimization is the root of all evil. Yet we should not pass up our opportunities in that critical 3%.\" -Knuth\n\n* You’re going to forget what your code does in a few months: make it easy to read.\n\n* Sometimes, all you need is a one-liner in `sed`\n\n* Beware of programmers who speak in absolutes. Programming is an art, not a religion.\n\n* If you know you will do a fixed sequence of steps more than ten times, automate it.\n\n* Backing it up is one thing. Restoring it is another.\n\n* Just because it works on your machine does not mean there is not a bug. -Piers Sutton\n\n* Wait for the point-one release of development tools before installing them. Let other people be guinea pigs.\n\n* Good programmers write good code. Great programmers write no code. Zen programmers delete code.\n\n* Lock in on one task and focus on completing it quickly and correctly, to the exclusion of all others. Every successful person does this.\n\n* Don't memorize commands or syntax; memorize concepts. Concentrate on problem solving.\n\n* When really stuck, go beyond questioning the code: question your assumptions about what the code does.\n\n* When you get overwhelmed by using new software just remember that you simply have to learn its API. Understand what it does, then learn its interface.\n\n* Ask yourself how hard your code will be to change later. Remember code is cheap.\n\n* \"After you have left, remember to look back into the abyss.\" - Douglas Crockford\n\n* Almost everything `rms` has ever said re: proprietary software.\n\n### (▀̿Ĺ̯▀̿ ̿)",
      "json_metadata": "{\"tags\":[\"technology\",\"programming\"],\"app\":\"steemit/0.1\",\"format\":\"markdown\"}",
      "parent_author": "",
      "parent_permlink": "technology",
      "permlink": "heuristics-on-the-practice-of-programming",
      "title": "Heuristics on the Practice of Programming"
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2018-09-04T21:30:27",
  "trx_id": "c64e0c9b15136348dfec2ef6abdabe06794d91bf",
  "trx_in_block": 50,
  "virtual_op": 0
}
2018/07/24 18:25:51
authorberniesanders
permlinkwhat-are-you-puffin-on
voterdavidpm
weight10000 (100.00%)
Transaction InfoBlock #24463348/Trx c6571811ff46b9215e108bbc26d4991e662f3199
View Raw JSON Data
{
  "block": 24463348,
  "op": [
    "vote",
    {
      "author": "berniesanders",
      "permlink": "what-are-you-puffin-on",
      "voter": "davidpm",
      "weight": 10000
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2018-07-24T18:25:51",
  "trx_id": "c6571811ff46b9215e108bbc26d4991e662f3199",
  "trx_in_block": 37,
  "virtual_op": 0
}
davidpmreceived 0.001 SP curation reward for @amn / authentication-in-ruby-on-rails-from-scratch
2018/07/22 10:34:03
comment authoramn
comment permlinkauthentication-in-ruby-on-rails-from-scratch
curatordavidpm
reward2.027780 VESTS
Transaction InfoBlock #24396351/Virtual Operation #64
View Raw JSON Data
{
  "block": 24396351,
  "op": [
    "curation_reward",
    {
      "comment_author": "amn",
      "comment_permlink": "authentication-in-ruby-on-rails-from-scratch",
      "curator": "davidpm",
      "reward": "2.027780 VESTS"
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2018-07-22T10:34:03",
  "trx_id": "0000000000000000000000000000000000000000",
  "trx_in_block": 4294967295,
  "virtual_op": 64
}
2018/07/17 18:13:18
authoramn
permlinkauthentication-in-ruby-on-rails-from-scratch
voterdavidpm
weight10000 (100.00%)
Transaction InfoBlock #24261652/Trx 8d9a181a0c1a4296ff193eddb9c1286c767c21f2
View Raw JSON Data
{
  "block": 24261652,
  "op": [
    "vote",
    {
      "author": "amn",
      "permlink": "authentication-in-ruby-on-rails-from-scratch",
      "voter": "davidpm",
      "weight": 10000
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2018-07-17T18:13:18",
  "trx_id": "8d9a181a0c1a4296ff193eddb9c1286c767c21f2",
  "trx_in_block": 64,
  "virtual_op": 0
}
2018/07/17 18:12:24
authorkleric-lod
permlinkcrypto-currency-cryptocurrency-going-mainstream-2018-2
voterdavidpm
weight10000 (100.00%)
Transaction InfoBlock #24261634/Trx 5c41cc2d37cee4ca5a049d7630e4f8cfaa6f4a4a
View Raw JSON Data
{
  "block": 24261634,
  "op": [
    "vote",
    {
      "author": "kleric-lod",
      "permlink": "crypto-currency-cryptocurrency-going-mainstream-2018-2",
      "voter": "davidpm",
      "weight": 10000
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2018-07-17T18:12:24",
  "trx_id": "5c41cc2d37cee4ca5a049d7630e4f8cfaa6f4a4a",
  "trx_in_block": 47,
  "virtual_op": 0
}
steemdelegated 18.599 SP to @davidpm
2018/07/08 21:24:51
delegateedavidpm
delegatorsteem
vesting shares30234.011559 VESTS
Transaction InfoBlock #24006384/Trx ad811caffc1b6972ada8cf3d1eaf5b3b50f60015
View Raw JSON Data
{
  "block": 24006384,
  "op": [
    "delegate_vesting_shares",
    {
      "delegatee": "davidpm",
      "delegator": "steem",
      "vesting_shares": "30234.011559 VESTS"
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2018-07-08T21:24:51",
  "trx_id": "ad811caffc1b6972ada8cf3d1eaf5b3b50f60015",
  "trx_in_block": 11,
  "virtual_op": 0
}
2018/07/07 07:03:54
authordavidpm
permlinkgit-interactive-rebase-to-alter-commits
voterishan89
weight10000 (100.00%)
Transaction InfoBlock #23960380/Trx 45f6bd446b334a444134575bdba9bbcbd02b96fa
View Raw JSON Data
{
  "block": 23960380,
  "op": [
    "vote",
    {
      "author": "davidpm",
      "permlink": "git-interactive-rebase-to-alter-commits",
      "voter": "ishan89",
      "weight": 10000
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2018-07-07T07:03:54",
  "trx_id": "45f6bd446b334a444134575bdba9bbcbd02b96fa",
  "trx_in_block": 4,
  "virtual_op": 0
}
2018/07/07 07:03:45
authordavidpm
permlinkgit-interactive-rebase-to-alter-commits
votermymarketcloudhq
weight10000 (100.00%)
Transaction InfoBlock #23960377/Trx 0eb7d486dbbcc512c18378d4eac47cbc5d911846
View Raw JSON Data
{
  "block": 23960377,
  "op": [
    "vote",
    {
      "author": "davidpm",
      "permlink": "git-interactive-rebase-to-alter-commits",
      "voter": "mymarketcloudhq",
      "weight": 10000
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2018-07-07T07:03:45",
  "trx_id": "0eb7d486dbbcc512c18378d4eac47cbc5d911846",
  "trx_in_block": 41,
  "virtual_op": 0
}
2018/07/07 07:02:33
authordavidpm
bodyDo you have skeletons in your git history? Maybe you want to re-write that history? I know I've been down that road, and since I've had to look up how to do this several times I thought it was high time to do a quick post. ![Erase-The-Past.jpg](https://cdn.steemitimages.com/DQmWJqJjDB6mm7eRds4KACk5LjtvoBdUsU5K7VCmPx2AC5Y/Erase-The-Past.jpg) For the uninitiated, [git](https://git-scm.com/) is powerful version control software written by [Linus Torvalds](https://en.wikipedia.org/wiki/Linus_Torvalds), who is most famous for writing the Linux kernel. If you are not familiar git, or version control this post will fly over your head. ## Poem Let's start writing a poem that we want under version control. Create a directory with an empty file: ``` > mkdir -p poem-git-example/poem && cd poem-git-example ``` Now let's write some [poetry](https://en.wikipedia.org/wiki/Les_Fleurs_du_mal): ``` # poem Hypocrite lecteur, — mon semblable, — mon frère! ``` Okay, so far we only have a one-line poem, but it looks like a classic. Let's commit our work: ``` > git init > git add . > git commit -m 'initial commit of poem' [master (root-commit) 5254598] initial commit of poem 1 file changed, 1 insertion(+) create mode 100644 poem ``` Let's make some changes. I want to add a title to the poem: ``` Les Fluers du Mal Hypocrite lecteur, — mon semblable, — mon frère! ``` Better put that under version control, too. ``` > git add . > git commit -m 'add title' ``` You should now have two commits. Let's just add one more thing: ``` Les Fluers du Mal Hypocrite lecteur, — mon semblable, — mon frère! - Charles Baudelaire ``` Okay, so its not mine. In fact, its not even part of the poem I stole it from. Its just one line from the preface but I like it so what? This is about git, so stay on task. Commit that last change: `git add . && git commit -m 'add signature'`. We now have three commits and a one-line poem, complete with title and signature (^̮^) But wait! looking back at our poem we have made an embarrassing typo. It should be `Fleurs` not `Fluers`! I could just correct the error and commit the changes and move on with my life. If this weren't a contrived example, I would do just that, but this is a contrived example. So, let's just say we are embarrassed that we left that punctuation out. We don't want that in our git history. What we'd like to do is go back to the original commit, amend the contents of that commit, leave the additions of the title and signature in place, and then carry on. ![](https://media.giphy.com/media/1M2BVyXFvMbYY/giphy.gif) There are two main ways to integrate changes from one branch into another using git: merging and rebasing. If you are unfamiliar with rebasing you should definitely [read up about it](https://www.atlassian.com/git/tutorials/rewriting-history/git-rebase). It's a great feature and nobody's <em>git-fu<em> is complete without the rebase. I'll be assuming you have experience with rebasing going forward. What we really need here to achieve our aims is something called the interactive rebase. We have three total commits, and we need to go back to that second commit where we added the misspelled title. We can use [treeish](https://git-scm.com/docs/gitglossary#gitglossary-aiddeftree-ishatree-ishalsotreeish) to walk back two commits inclusive from `HEAD`: ``` git rebase --interactive HEAD~2 # can also do: # git rebase -i HEAD~2 # or if the second commit has a SHA: f3626e9 # git rebase -i 'f3626e9^' ``` This will open an interactive prompt in your default text editor, or the one you defined in your git config. It should look something like this: ``` pick f3626e9 add title pick 4bef291 add signature # Rebase 5254598..4bef291 onto 5254598 (2 commands) # # Commands: # p, pick = use commit # r, reword = use commit, but edit the commit message # e, edit = use commit, but stop for amending # s, squash = use commit, but meld into previous commit # f, fixup = like "squash", but discard this commit's log message # x, exec = run command (the rest of the line) using shell # d, drop = remove commit # # These lines can be re-ordered; they are executed from top to bottom. # # If you remove a line here THAT COMMIT WILL BE LOST. # # However, if you remove everything, the rebase will be aborted. # # Note that empty commits are commented out ``` Take note of the helpful comments. Basically, we need to identify the commit we want to edit, change the word `pick` to `edit`, and then save the file. If we leave a commit as `pick` we'll use the changes from that commit as they are. If we say `edit` we'll be allowed to go make our edits, and rewrite history. ``` pick f3626e9 add title edit 4bef291 add signature # Rebase 5254598..4bef291 onto 5254598 (2 commands) # # Commands: # p, pick = use commit # r, reword = use commit, but edit the commit message # e, edit = use commit, but stop for amending # s, squash = use commit, but meld into previous commit # f, fixup = like "squash", but discard this commit's log message # x, exec = run command (the rest of the line) using shell # d, drop = remove commit # # These lines can be re-ordered; they are executed from top to bottom. # # If you remove a line here THAT COMMIT WILL BE LOST. # # However, if you remove everything, the rebase will be aborted. # # Note that empty commits are commented out ``` Save that and you should see some output like this: ``` Stopped at 4bef291... add signature You can amend the commit now, with git commit --amend Once you are satisfied with your changes, run git rebase --continue ``` This means we are free to make our changes. Open up poem and fix that typo `Fluers -> Fleurs`: ``` Les Fleurs du Mal Hypocrite lecteur, — mon semblable, — mon frère! - Charles Baudelaire ``` Now, we need to remember what our prompt was telling us. We can amend the commit with: `git commit --amend`. That's great and will open another prompt in your editor and ask you to update the commit message. In our case, we just want to use the same message, so we're going to add a couple flags to our command to skip ahead: ``` git commit --all --amend --no-edit ``` So, we have now updated that second commit with the correct spelling. But if you run `git status` you'll notice we are in a detached HEAD state. We want to move HEAD back to our third commit where we were. Once again, we can just follow the prompt and run `git rebase --continue` to return back to the previous commit HEAD. That's all there is to it! Now, this kind of fancy footwork will end up changing the SHA-1 of that commit and rewrites the history from that point forward. You can break repositories doing things like this, and is best to avoid it when you can. If you have a trunk/master branch that is immutable then you will be blocked from a repo admin from trying something like anyways. Having said all that, I'm sure you can see that this can come in handy in a pinch. Until next time! ### | (• ◡•)| (❍ᴥ❍ʋ)
json metadata{"tags":["programming","git","rebase"],"image":["https://cdn.steemitimages.com/DQmWJqJjDB6mm7eRds4KACk5LjtvoBdUsU5K7VCmPx2AC5Y/Erase-The-Past.jpg","https://media.giphy.com/media/1M2BVyXFvMbYY/giphy.gif"],"links":["https://git-scm.com/","https://en.wikipedia.org/wiki/Linus_Torvalds","https://en.wikipedia.org/wiki/Les_Fleurs_du_mal","https://www.atlassian.com/git/tutorials/rewriting-history/git-rebase","https://git-scm.com/docs/gitglossary#gitglossary-aiddeftree-ishatree-ishalsotreeish"],"app":"steemit/0.1","format":"markdown"}
parent author
parent permlinkprogramming
permlinkgit-interactive-rebase-to-alter-commits
titleGit Interactive Rebase to Alter Commits
Transaction InfoBlock #23960353/Trx 56fcc41411a69589ad1931c400f2ddc1dd3682b7
View Raw JSON Data
{
  "block": 23960353,
  "op": [
    "comment",
    {
      "author": "davidpm",
      "body": "Do you have skeletons in your git history? Maybe you want to re-write that history? I know I've been down that road, and since I've had to look up how to do this several times I thought it was high time to do a quick post.\n\n![Erase-The-Past.jpg](https://cdn.steemitimages.com/DQmWJqJjDB6mm7eRds4KACk5LjtvoBdUsU5K7VCmPx2AC5Y/Erase-The-Past.jpg)\n\nFor the uninitiated, [git](https://git-scm.com/) is powerful version control software written by [Linus Torvalds](https://en.wikipedia.org/wiki/Linus_Torvalds), who is most famous for writing the Linux kernel. If you are not familiar git, or version control this post will fly over your head.\n\n## Poem\n\nLet's start writing a poem that we want under version control. Create a directory with an empty file:\n```\n> mkdir -p poem-git-example/poem && cd poem-git-example\n```\nNow let's write some [poetry](https://en.wikipedia.org/wiki/Les_Fleurs_du_mal):\n```\n# poem\nHypocrite lecteur, — mon semblable, — mon frère!\n```\nOkay, so far we only have a one-line poem, but it looks like a classic. Let's commit our work:\n```\n> git init\n> git add .\n> git commit -m 'initial commit of poem'\n\n[master (root-commit) 5254598] initial commit of poem\n 1 file changed, 1 insertion(+)\n create mode 100644 poem\n```\nLet's make some changes. I want to add a title to the poem:\n```\nLes Fluers du Mal\n\nHypocrite lecteur, — mon semblable, — mon frère!\n```\nBetter put that under version control, too.\n```\n> git add .\n> git commit -m 'add title'\n```\nYou should now have two commits. Let's just add one more thing:\n```\nLes Fluers du Mal\n\nHypocrite lecteur, — mon semblable, — mon frère!\n\n- Charles Baudelaire\n```\nOkay, so its not mine. In fact, its not even part of the poem I stole it from. Its just one line from the preface but I like it so what? This is about git, so stay on task. \n\nCommit that last change: `git add . && git commit -m 'add signature'`. We now have three commits and a one-line poem, complete with title and signature (^̮^) But wait! looking back at our poem we have made an embarrassing typo. It should be `Fleurs` not `Fluers`! I could just correct the error and commit the changes and move on with my life. If this weren't a contrived example, I would do just that, but this is a contrived example. \n\nSo, let's just say we are embarrassed that we left that punctuation out. We don't want that in our git history. What we'd like to do is go back to the original commit, amend the contents of that commit, leave the additions of the title and signature in place, and then carry on.\n\n![](https://media.giphy.com/media/1M2BVyXFvMbYY/giphy.gif)\n\nThere are two main ways to integrate changes from one branch into another using git: merging and rebasing. If you are unfamiliar with rebasing you should definitely [read up about it](https://www.atlassian.com/git/tutorials/rewriting-history/git-rebase). It's a great feature and nobody's <em>git-fu<em> is complete without the rebase. I'll be assuming you have experience with rebasing going forward.\n\nWhat we really need here to achieve our aims is something called the interactive rebase. We have three total commits, and we need to go back to that second commit where we added the misspelled title. We can use [treeish](https://git-scm.com/docs/gitglossary#gitglossary-aiddeftree-ishatree-ishalsotreeish) to walk back two commits inclusive from `HEAD`:\n\n```\ngit rebase --interactive HEAD~2\n\n# can also do:\n# git rebase -i HEAD~2\n\n# or if the second commit has a SHA: f3626e9\n# git rebase -i 'f3626e9^'\n```\n\nThis will open an interactive prompt in your default text editor, or the one you defined in your git config. It should look something like this:\n\n```\npick f3626e9 add title\npick 4bef291 add signature\n\n# Rebase 5254598..4bef291 onto 5254598 (2 commands)\n#\n# Commands:\n# p, pick = use commit\n# r, reword = use commit, but edit the commit message\n# e, edit = use commit, but stop for amending\n# s, squash = use commit, but meld into previous commit\n# f, fixup = like \"squash\", but discard this commit's log message\n# x, exec = run command (the rest of the line) using shell\n# d, drop = remove commit\n#\n# These lines can be re-ordered; they are executed from top to bottom.\n#\n# If you remove a line here THAT COMMIT WILL BE LOST.\n#\n# However, if you remove everything, the rebase will be aborted.\n#\n# Note that empty commits are commented out\n```\nTake note of the helpful comments. Basically, we need to identify the commit we want to edit, change the word `pick` to `edit`, and then save the file. If we leave a commit as `pick` we'll use the changes from that commit as they are. If we say `edit` we'll be allowed to go make our edits, and rewrite history.\n```\npick f3626e9 add title\nedit 4bef291 add signature\n\n# Rebase 5254598..4bef291 onto 5254598 (2 commands)\n#\n# Commands:\n# p, pick = use commit\n# r, reword = use commit, but edit the commit message\n# e, edit = use commit, but stop for amending\n# s, squash = use commit, but meld into previous commit\n# f, fixup = like \"squash\", but discard this commit's log message\n# x, exec = run command (the rest of the line) using shell\n# d, drop = remove commit\n#\n# These lines can be re-ordered; they are executed from top to bottom.\n#\n# If you remove a line here THAT COMMIT WILL BE LOST.\n#\n# However, if you remove everything, the rebase will be aborted.\n#\n# Note that empty commits are commented out\n```\nSave that and you should see some output like this:\n```\nStopped at 4bef291...  add signature\nYou can amend the commit now, with\n\n  git commit --amend\n\nOnce you are satisfied with your changes, run\n\n  git rebase --continue\n```\nThis means we are free to make our changes. Open up poem and fix that typo `Fluers -> Fleurs`:\n```\nLes Fleurs du Mal\n\nHypocrite lecteur, — mon semblable, — mon frère!\n\n- Charles Baudelaire\n```\nNow, we need to remember what our prompt was telling us. We can amend the commit with: `git commit --amend`. That's great and will open another prompt in your editor and ask you to update the commit message. In our case, we just want to use the same message, so we're going to add a couple flags to our command to skip ahead:\n```\ngit commit --all --amend --no-edit\n```\nSo, we have now updated that second commit with the correct spelling. But if you run `git status` you'll notice we are in a detached HEAD state. We want to move HEAD back to our third commit where we were. Once again, we can just follow the prompt and run `git rebase --continue` to return back to the previous commit HEAD.\n\nThat's all there is to it! Now, this kind of fancy footwork will end up changing the SHA-1 of that commit and rewrites the history from that point forward. You can break repositories doing things like this, and is best to avoid it when you can. If you have a trunk/master branch that is immutable then you will be blocked from a repo admin from trying something like anyways. Having said all that, I'm sure you can see that this can come in handy in a pinch. Until next time!\n\n### | (• ◡•)| (❍ᴥ❍ʋ)",
      "json_metadata": "{\"tags\":[\"programming\",\"git\",\"rebase\"],\"image\":[\"https://cdn.steemitimages.com/DQmWJqJjDB6mm7eRds4KACk5LjtvoBdUsU5K7VCmPx2AC5Y/Erase-The-Past.jpg\",\"https://media.giphy.com/media/1M2BVyXFvMbYY/giphy.gif\"],\"links\":[\"https://git-scm.com/\",\"https://en.wikipedia.org/wiki/Linus_Torvalds\",\"https://en.wikipedia.org/wiki/Les_Fleurs_du_mal\",\"https://www.atlassian.com/git/tutorials/rewriting-history/git-rebase\",\"https://git-scm.com/docs/gitglossary#gitglossary-aiddeftree-ishatree-ishalsotreeish\"],\"app\":\"steemit/0.1\",\"format\":\"markdown\"}",
      "parent_author": "",
      "parent_permlink": "programming",
      "permlink": "git-interactive-rebase-to-alter-commits",
      "title": "Git Interactive Rebase to Alter Commits"
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2018-07-07T07:02:33",
  "trx_id": "56fcc41411a69589ad1931c400f2ddc1dd3682b7",
  "trx_in_block": 26,
  "virtual_op": 0
}
davidpmreceived 0.001 STEEM, 0.006 SBD, 0.006 SP author reward for @davidpm / create-a-command-line-gem-from-scratch-with-thor
2018/06/10 06:17:51
authordavidpm
permlinkcreate-a-command-line-gem-from-scratch-with-thor
sbd payout0.006 SBD
steem payout0.001 STEEM
vesting payout10.161328 VESTS
Transaction InfoBlock #23192516/Virtual Operation #4
View Raw JSON Data
{
  "block": 23192516,
  "op": [
    "author_reward",
    {
      "author": "davidpm",
      "permlink": "create-a-command-line-gem-from-scratch-with-thor",
      "sbd_payout": "0.006 SBD",
      "steem_payout": "0.001 STEEM",
      "vesting_payout": "10.161328 VESTS"
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2018-06-10T06:17:51",
  "trx_id": "0000000000000000000000000000000000000000",
  "trx_in_block": 4294967295,
  "virtual_op": 4
}
2018/06/04 00:12:27
idfollow
json["follow",{"follower":"davidpm","following":"radiumbattery","what":["blog"]}]
required auths[]
required posting auths["davidpm"]
Transaction InfoBlock #23012467/Trx 87ba04c2f307f7c4385f4ad800ca44c76b047aaf
View Raw JSON Data
{
  "block": 23012467,
  "op": [
    "custom_json",
    {
      "id": "follow",
      "json": "[\"follow\",{\"follower\":\"davidpm\",\"following\":\"radiumbattery\",\"what\":[\"blog\"]}]",
      "required_auths": [],
      "required_posting_auths": [
        "davidpm"
      ]
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2018-06-04T00:12:27",
  "trx_id": "87ba04c2f307f7c4385f4ad800ca44c76b047aaf",
  "trx_in_block": 24,
  "virtual_op": 0
}
davidpmupvoted (100.00%) @radiumbattery / jade-hunt
2018/06/04 00:12:18
authorradiumbattery
permlinkjade-hunt
voterdavidpm
weight10000 (100.00%)
Transaction InfoBlock #23012464/Trx 60ac81ae1604a239c82ab88b2d412859de2338f4
View Raw JSON Data
{
  "block": 23012464,
  "op": [
    "vote",
    {
      "author": "radiumbattery",
      "permlink": "jade-hunt",
      "voter": "davidpm",
      "weight": 10000
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2018-06-04T00:12:18",
  "trx_id": "60ac81ae1604a239c82ab88b2d412859de2338f4",
  "trx_in_block": 31,
  "virtual_op": 0
}
2018/06/03 21:35:09
authordavidpm
permlinkdropping-into-b-trees
voterlionindayard
weight49 (0.49%)
Transaction InfoBlock #23009322/Trx 2466fa51b78c0dac08c770531b8f90d1ad699d14
View Raw JSON Data
{
  "block": 23009322,
  "op": [
    "vote",
    {
      "author": "davidpm",
      "permlink": "dropping-into-b-trees",
      "voter": "lionindayard",
      "weight": 49
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2018-06-03T21:35:09",
  "trx_id": "2466fa51b78c0dac08c770531b8f90d1ad699d14",
  "trx_in_block": 45,
  "virtual_op": 0
}
2018/06/03 21:35:09
authordavidpm
permlinkdropping-into-b-trees
votermarketstack
weight49 (0.49%)
Transaction InfoBlock #23009322/Trx a01b75f25654ca838cc1c4d751efdd6ee54bdbe2
View Raw JSON Data
{
  "block": 23009322,
  "op": [
    "vote",
    {
      "author": "davidpm",
      "permlink": "dropping-into-b-trees",
      "voter": "marketstack",
      "weight": 49
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2018-06-03T21:35:09",
  "trx_id": "a01b75f25654ca838cc1c4d751efdd6ee54bdbe2",
  "trx_in_block": 12,
  "virtual_op": 0
}
2018/06/03 21:27:54
authordavidpm
permlinkdropping-into-b-trees
voterswagger
weight3 (0.03%)
Transaction InfoBlock #23009177/Trx 00c2c90a51b4179ce366b87c7276eb0131f053a4
View Raw JSON Data
{
  "block": 23009177,
  "op": [
    "vote",
    {
      "author": "davidpm",
      "permlink": "dropping-into-b-trees",
      "voter": "swagger",
      "weight": 3
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2018-06-03T21:27:54",
  "trx_id": "00c2c90a51b4179ce366b87c7276eb0131f053a4",
  "trx_in_block": 25,
  "virtual_op": 0
}
davidpmpublished a new post: dropping-into-b-trees
2018/06/03 21:06:12
authordavidpm
bodyMy recent (and only) RailsConf talk: https://www.youtube.com/watch?v=17XecHy9Pzg
json metadata{"tags":["technology","rails","ruby","databases","programming"],"image":["https://img.youtube.com/vi/17XecHy9Pzg/0.jpg"],"links":["https://www.youtube.com/watch?v=17XecHy9Pzg"],"app":"steemit/0.1","format":"markdown"}
parent author
parent permlinktechnology
permlinkdropping-into-b-trees
titleDropping Into B-trees
Transaction InfoBlock #23008743/Trx d2440d3bc8ae4f60632f99e3f39a4844f4b6224a
View Raw JSON Data
{
  "block": 23008743,
  "op": [
    "comment",
    {
      "author": "davidpm",
      "body": "My recent (and only) RailsConf talk:\n\nhttps://www.youtube.com/watch?v=17XecHy9Pzg",
      "json_metadata": "{\"tags\":[\"technology\",\"rails\",\"ruby\",\"databases\",\"programming\"],\"image\":[\"https://img.youtube.com/vi/17XecHy9Pzg/0.jpg\"],\"links\":[\"https://www.youtube.com/watch?v=17XecHy9Pzg\"],\"app\":\"steemit/0.1\",\"format\":\"markdown\"}",
      "parent_author": "",
      "parent_permlink": "technology",
      "permlink": "dropping-into-b-trees",
      "title": "Dropping Into B-trees"
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2018-06-03T21:06:12",
  "trx_id": "d2440d3bc8ae4f60632f99e3f39a4844f4b6224a",
  "trx_in_block": 29,
  "virtual_op": 0
}
2018/06/03 07:35:12
authordavidpm
permlinkcreate-a-command-line-gem-from-scratch-with-thor-part-two
votermagpielover
weight10000 (100.00%)
Transaction InfoBlock #22992523/Trx 14ccfee4d8c44df15fe9cef575df3418097af1b3
View Raw JSON Data
{
  "block": 22992523,
  "op": [
    "vote",
    {
      "author": "davidpm",
      "permlink": "create-a-command-line-gem-from-scratch-with-thor-part-two",
      "voter": "magpielover",
      "weight": 10000
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2018-06-03T07:35:12",
  "trx_id": "14ccfee4d8c44df15fe9cef575df3418097af1b3",
  "trx_in_block": 1,
  "virtual_op": 0
}
2018/06/03 07:24:51
authordavidpm
permlinkcreate-a-command-line-gem-from-scratch-with-thor-part-two
voterlionindayard
weight49 (0.49%)
Transaction InfoBlock #22992316/Trx 07cf29dc61c2d8a9a7ffc0411e65091747a3b9b5
View Raw JSON Data
{
  "block": 22992316,
  "op": [
    "vote",
    {
      "author": "davidpm",
      "permlink": "create-a-command-line-gem-from-scratch-with-thor-part-two",
      "voter": "lionindayard",
      "weight": 49
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2018-06-03T07:24:51",
  "trx_id": "07cf29dc61c2d8a9a7ffc0411e65091747a3b9b5",
  "trx_in_block": 38,
  "virtual_op": 0
}
2018/06/03 07:24:51
authordavidpm
permlinkcreate-a-command-line-gem-from-scratch-with-thor-part-two
votermarketstack
weight49 (0.49%)
Transaction InfoBlock #22992316/Trx 789482d07c1a6cd5e8fc8d118820a2aed5e3419a
View Raw JSON Data
{
  "block": 22992316,
  "op": [
    "vote",
    {
      "author": "davidpm",
      "permlink": "create-a-command-line-gem-from-scratch-with-thor-part-two",
      "voter": "marketstack",
      "weight": 49
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2018-06-03T07:24:51",
  "trx_id": "789482d07c1a6cd5e8fc8d118820a2aed5e3419a",
  "trx_in_block": 2,
  "virtual_op": 0
}
2018/06/03 07:17:48
authordavidpm
bodyThis is the last in a two-part post on creating a Ruby cli gem from scratch. Part one <a href='https://steemit.com/ruby/@davidpm/create-a-command-line-gem-from-scratch-with-thor'>found here</a>. --- Picking up from our <a href='https://steemit.com/ruby/@davidpm/create-a-command-line-gem-from-scratch-with-thor'>last post</a>, we want to start working on our user interface. In this case we want a cli that looks something like this: ``` #-> what would you like to do? 1 - Build a matrix 2 - Generate a random matrix #-> now that we have a matrix, would you like to spiralize it? 1 - Spiralize it 2 - Stare at it ``` ### Thor ![](https://ae01.alicdn.com/kf/HTB1FjN5KFXXXXa6XFXXq6xXFXXXx/1-1-Scale-Full-Metal-Thor-Hammer-Mjolnir-1-1-Replica-Thor-Custom-Cosplay-Hammer-Collection.jpg_640x640.jpg) We're going to use <a href='https://github.com/erikhuda/thor'>Thor</a>, which is a toolkit for building command-line tools and applications. In fact, `Bundler` itself was written using `Thor`. Let's add it to the bottom of our `gemspec` and re-install: ``` # spiralizer.gemspec spec.add_dependency "thor" --- # from cmd line: > bundle install ``` Now, we just need to require it in and start using it! ``` require 'thor' ... ``` But, this is also a good time to start separating our code a bit, so we don't let things get to messy. So, let's create a file called `lib/spiralizer/cli.rb`, and move our `require` statement into this file. Then we will stub out a class under the `Spiralizer` namespace and inherit from `Thor`: ``` require 'thor' require 'spiralizer' class Spiralizer::CLI < Thor end ``` Back in our main module we will need to require in this new file: ``` require "spiralizer/version" require "spiralizer/cli" module Spiralizer ... end ``` Our cli is gem that users can install on their systems. We want them to have an executable that will take a simple command to spin up prompt we defined earlier: Say the user enters `spiralizer go` for example. We would want output similar to what we envisioned above: ``` $ >spiralizer go #-> what would you like to do? 1 - Build a matrix 2 - Generate a random matrix ``` Thor gives us some easy-to-use tooling to get this thing going in no time. We'll get started by defining our `go` method, and use the `desc` macro annotation get some magic documentation: ``` require 'thor' require 'spiralizer' class Spiralizer::CLI < Thor desc 'go', 'starts a prompt that brings users to spiral heaven' def go say "What would you like to do? (choose a number)\n 1 - Build a matrix\n 2 - Generate a random matrix" action = ask '> ' say "\nyou picked #{action}" exit! end end ``` `Thor` provides some intuitive methods like `say` to output something to the user, and `ask` to prompt the user for input. They work just as expected. Notice that you can assign the result of `ask` to a variable. To start playing around with it, we just need to create a new directory called `exe` (alternatively, we could have scaffolded our gem with a `--exe` option but here we are). It should be a sibling directory of `lib` and `bin`. `cd exe && touch spiralizer` to create a file. Then, give it executable permissions: `> chmod +x exe/spiralizer`, and dump this inside: ``` #!/usr/bin/env ruby require 'spiralizer/cli' Spiralizer::CLI.start ``` And let's give 'er a whirl: ``` > bundle exec exe/spiralizer Commands: spiralizer go # starts a prompt that brings users to spiral heaven spiralizer help [COMMAND] # Describe available commands or one specific command ``` Notice the helpful output Thor gives us. We have two commands available: `go` and `help`. Let's try them out: ``` > bundle exec exe/spiralizer help Commands: spiralizer go # starts a prompt that brings users to spiral heaven spiralizer help [COMMAND] # Describe available commands or one specific command > bundle exec exe/spiralizer go What would you like to do? (choose a number) 1 - Build a matrix 2 - Generate a random matrix > 1 you picked 1 ``` So, `help` is the default action, and we can see our new `go` command is registered properly. Right now our prompt gives us two options, let's add functionality for our second option. When a user enters `'2'` we want to spit out a random matrix. We can do a quick and dirty version of this and just hard code some matrix range/dimension pairs that we will feed to our matrix factory. Let's make a constant called `MATRICES` that itself is a matrix: ``` MATRICES = [ ['A-L', '4x3'], ['M-X', '3x4'], ['1-8', '2x4'], ['1-144', '12x12'], ['C-J', '4x2'], ['A-Z', '13x2'] ].freeze ``` We now need a semi-random way to pick one of these: `range_response, dimensions = MATRICES[rand(6)]`. This will give us a range value as a string like 'C-j' and dimensions like `4x2`. We'll then split that range string up, so we can feed it into our factory: ``` range = range_response.split('-') matrix = Spiralizer::Matrix.the_matrix(range: (range.first..range.last), dimensions: dimensions) ``` `Thor` wants you to put helper methods for your class in a `no_commands` block, so let's create one of those and put all this together so its somewhat presentable. Here's what we have so far: ``` class Spiralizer::CLI < Thor attr_reader :matrix MATRICES = [ ['A-L', '4x3'], ['M-X', '3x4'], ['1-8', '2x4'], ['1-144', '12x12'], ['C-J', '4x2'], ['A-Z', '13x2'] ].freeze desc 'go', 'starts a prompt that brings users to spiral heaven' def go say "What would you like to do? (choose a number)\n 1 - Build a matrix\n 2 - Generate a random matrix" action = ask '> ' say "\n" range, dimensions = random_range_and_dimensions @matrix = Spiralizer::Matrix.the_matrix(range: range, dimensions: dimensions) output_matrix end no_commands do def random_range_and_dimensions say "\ngenerating..." sleep 1 range_response, dimensions = MATRICES[rand(6)] range = range_response.split('-') return (range.first..range.last), dimensions end def output_matrix say "\nHere is your M A T R I X\n------------------------" matrix.each { |inner| say inner.join("\t") } exit! end end end ``` You'll notice that any input we give the prompt at this point will just generate a random matrix from our list. We're making progress! ``` > bundle exec exe/spiralizer go What would you like to do? (choose a number) 1 - Build a matrix 2 - Generate a random matrix > 2 generating... Here is your M A T R I X ------------------------ 1 2 3 4 5 6 7 8 ``` Next, let's hook up a follow up prompt that asks us if we want to spiralize the matrix. Update your `output_matrix` method: ``` def output_matrix say "\nHere is your M A T R I X\n------------------------" matrix.each { |arr| say arr.join("\t") } say "\nWould you like to spiralize it? (choose a number)\n 1 - Yes\n 2 - No" action = ask '> ' say "\n" if action == '1' say Spiralizer::Spiralize.new(matrix: matrix).perform end exit! end ``` We are now prompted for more input after the matrix has been generated! Pretty neat. ``` > bundle exec exe/spiralizer go What would you like to do? (choose a number) 1 - Build a matrix 2 - Generate a random matrix > 2 generating... Here is your M A T R I X ------------------------ M N O P Q R S T U V W X Would you like to spiralize it? (choose a number) 1 - Yes 2 - No > 1 m n o r u x w v s p q t ``` This is looking good but we still need to handle our first option to allow users to create their own matrix. Let's add a method to handle that input now: ``` def user_range_and_dimensions range_response = ask("Please provide range. Acceptable format: (A-L) or (1-6)\n> ") values = range_response.split('-') dimensions = ask("Please provide dimensions. Acceptable format: (4x3) or (3x2)\n> ") return (values.first..values.last), dimensions end ``` We want this to trigger when the user chooses `'1'` from our introduction prompt so we need to re-work our `go` method: ``` def go say "What would you like to do? (choose a number)\n 1 - Build a matrix\n 2 - Generate a random matrix" action = ask '> ' say "\n" case action when '1' then range, dimensions = user_range_and_dimensions when '2' then range, dimensions = random_range_and_dimensions else exit! end range, dimensions = random_range_and_dimensions @matrix = Spiralizer::Matrix.the_matrix(range: range, dimensions: dimensions) output_matrix rescue Spiralizer::InvalidInput => e say "\nUh oh! #{e.message}" ensure say "\nexiting..." exit! end ``` You should be getting the hang of how to use `Thor`. Our class is need of some cleanup, but with some refactoring we end up with something like this: ``` require 'thor' require 'spiralizer' class Spiralizer::CLI < Thor MATRICES = [ ['A-L', '4x3'], ['M-X', '3x4'], ['1-8', '2x4'], ['1-144', '12x12'], ['C-J', '4x2'], ['A-Z', '13x2'] ].freeze attr_reader :action, :matrix desc 'go', 'starts a prompt that brings users to spiral heaven' def go clear_screen! intro_prompt build_matrix output_matrix action_prompt spiralize! rescue Spiralizer::InvalidInput => e say "\nUh oh! #{e.message}" ensure quit_softly end no_commands do def intro_prompt say "What would you like to do? (choose a number)\n 1 - Build a matrix\n 2 - Generate a random matrix" @action = ask '> ' end def build_matrix say "\n" case action when '1' then range, dimensions = user_range_and_dimensions when '2' then range, dimensions = random_range_and_dimensions else quit_softly end @matrix = Spiralizer::Matrix.the_matrix(range: range, dimensions: dimensions) end def user_range_and_dimensions range_response = ask("Please provide range. Acceptable format: (A-L) or (1-6)\n> ") values = range_response.split('-') dimensions = ask("Please provide dimensions. Acceptable format: (4x3) or (3x2)\n> ") return (values.first..values.last), dimensions end def random_range_and_dimensions say "\ngenerating..." pause_for_effect range_response, dimensions = MATRICES[rand(6)] range = range_response.split('-') return (range.first..range.last), dimensions end def action_prompt say "\nWhat would you like to do with it? (choose a number)\n 1 - Spiralize it\n 2 - Look at it" @action = ask '> ' end def output_matrix say "\nHere is your M A T R I X\n------------------------" matrix.each { |inner| say inner.join("\t") } end def spiralize! say "\n" say Spiralizer::Spiralize.new(matrix: matrix).perform if action == '1' end def clear_screen! return system 'cls' if Gem.win_platform? system 'clear' end def pause_for_effect sleep 0.5 end def quit_softly say "\nexiting..." pause_for_effect exit! end end end ``` Now, let's try it all out: ``` What would you like to do? (choose a number) 1 - Build a matrix 2 - Generate a random matrix > 1 Please provide range. Acceptable format: (A-L) or (1-6) > 1-6 Please provide dimensions. Acceptable format: (4x3) or (3x2) > 2x3 Here is your M A T R I X ------------------------ 1 2 3 4 5 6 What would you like to do with it? (choose a number) 1 - Spiralize it 2 - Look at it > 1 1 2 4 6 5 3 exiting... ``` Our little gem is complete! There is a lot of cleanup we can and should do, but this is where this blog post ends. Feel free to add new functionality and experiment further with Thor. I've gone ahead and added a `Crisscross` class for example in my <a href="https://github.com/david-pm/matrix-spiralizer">repo</a>. Oh! One more thing, if yoy run `bundle exec rake install` bundler will install this gem on your system as a global executable so you can then call it like this: ``` > spiralizer go ``` If you ever need to uninstall it for some insane reason just run `gem uninstall spiralizer`. You can find a finalized version of the code <a href="https://github.com/david-pm/matrix-spiralizer">here</a>. Well, there you have it. We've built a cli gem from scratch using `Bundler` and `Thor`. I hope this post serves you well and until next time! ### (づ。◕‿‿◕。)づ *:・゚✧ ✧゚・*:・゚✧ ✧゚・
json metadata{"tags":["technology","ruby","thor","programming"],"image":["https://ae01.alicdn.com/kf/HTB1FjN5KFXXXXa6XFXXq6xXFXXXx/1-1-Scale-Full-Metal-Thor-Hammer-Mjolnir-1-1-Replica-Thor-Custom-Cosplay-Hammer-Collection.jpg_640x640.jpg"],"links":["https://steemit.com/ruby/@davidpm/create-a-command-line-gem-from-scratch-with-thor","https://github.com/erikhuda/thor","https://github.com/david-pm/matrix-spiralizer"],"app":"steemit/0.1","format":"markdown"}
parent author
parent permlinkruby
permlinkcreate-a-command-line-gem-from-scratch-with-thor-part-two
titleCreate a command-line gem from scratch with Thor (part two)
Transaction InfoBlock #22992175/Trx 7f9d967d5f2df769eec5c62c34679339a2ffca2a
View Raw JSON Data
{
  "block": 22992175,
  "op": [
    "comment",
    {
      "author": "davidpm",
      "body": "This is the last in a two-part post on creating a Ruby cli gem from scratch. Part one <a href='https://steemit.com/ruby/@davidpm/create-a-command-line-gem-from-scratch-with-thor'>found here</a>.\n\n---\n\nPicking up from our <a href='https://steemit.com/ruby/@davidpm/create-a-command-line-gem-from-scratch-with-thor'>last post</a>, we want to start working on our user interface. In this case we want a cli that looks something like this:\n```\n#-> what would you like to do?\n  1 - Build a matrix\n  2 - Generate a random matrix\n\n#-> now that we have a matrix, would you like to spiralize it?\n  1 - Spiralize it\n  2 - Stare at it\n```\n### Thor\n![](https://ae01.alicdn.com/kf/HTB1FjN5KFXXXXa6XFXXq6xXFXXXx/1-1-Scale-Full-Metal-Thor-Hammer-Mjolnir-1-1-Replica-Thor-Custom-Cosplay-Hammer-Collection.jpg_640x640.jpg)\n\nWe're going to use <a href='https://github.com/erikhuda/thor'>Thor</a>, which is a toolkit for building command-line tools and applications. In fact, `Bundler` itself was written using `Thor`. Let's add it to the bottom of our `gemspec` and re-install:\n\n```\n# spiralizer.gemspec\nspec.add_dependency \"thor\"\n---\n# from cmd line:\n> bundle install\n```\n\nNow, we just need to require it in and start using it!\n\n```\nrequire 'thor'\n...\n```\n\nBut, this is also a good time to start separating our code a bit, so we don't let things get to messy. So, let's create a file called `lib/spiralizer/cli.rb`, and move our `require` statement into this file. Then we will stub out a class under the `Spiralizer` namespace and inherit from `Thor`:\n\n```\nrequire 'thor'\nrequire 'spiralizer'\n\nclass Spiralizer::CLI < Thor\nend\n```\n\nBack in our main module we will need to require in this new file:\n\n```\nrequire \"spiralizer/version\"\nrequire \"spiralizer/cli\"\n\nmodule Spiralizer\n...\nend\n```\n\nOur cli is gem that users can install on their systems. We want them to have an executable that will take a simple command to spin up prompt we defined earlier:\n\nSay the user enters `spiralizer go` for example. We would want output similar to what we envisioned above:\n```\n$ >spiralizer go\n\n#-> what would you like to do?\n  1 - Build a matrix\n  2 - Generate a random matrix\n```\n\nThor gives us some easy-to-use tooling to get this thing going in no time. We'll get started by defining our `go` method, and use the `desc` macro annotation get some magic documentation:\n```\nrequire 'thor'\nrequire 'spiralizer'\n\nclass Spiralizer::CLI < Thor\n  desc 'go', 'starts a prompt that brings users to spiral heaven'\n  def go\n    say \"What would you like to do? (choose a number)\\n 1 - Build a matrix\\n 2 - Generate a random matrix\"\n    action = ask '> '\n    say \"\\nyou picked #{action}\"\n    exit!\n  end\nend\n```\n`Thor` provides some intuitive methods like `say` to output something to the user, and `ask` to prompt the user for input. They work just as expected. Notice that you can assign the result of `ask` to a variable.\n\nTo start playing around with it, we just need to create a new directory called `exe` (alternatively, we could have scaffolded our gem with a `--exe` option but here we are). It should be a sibling directory of `lib` and `bin`. `cd exe && touch spiralizer` to create a file. Then, give it executable permissions: `> chmod +x exe/spiralizer`, and dump this inside:\n```\n#!/usr/bin/env ruby\n\nrequire 'spiralizer/cli'\nSpiralizer::CLI.start\n```\n\n And let's give 'er a whirl:\n\n```\n> bundle exec exe/spiralizer\n\nCommands:\n  spiralizer go              # starts a prompt that brings users to spiral heaven\n  spiralizer help [COMMAND]  # Describe available commands or one specific command\n```\n\nNotice the helpful output Thor gives us. We have two commands available: `go` and `help`. Let's try them out:\n\n```\n> bundle exec exe/spiralizer help\n\nCommands:\n  spiralizer go              # starts a prompt that brings users to spiral heaven\n  spiralizer help [COMMAND]  # Describe available commands or one specific command\n\n> bundle exec exe/spiralizer go\n\nWhat would you like to do? (choose a number)\n 1 - Build a matrix\n 2 - Generate a random matrix\n>  1\n\nyou picked 1\n```\nSo, `help` is the default action, and we can see our new `go` command is registered properly. Right now our prompt gives us two options, let's add functionality for our second option. When a user enters `'2'` we want to spit out a random matrix. We can do a quick and dirty version of this and just hard code some matrix range/dimension pairs that we will feed to our matrix factory. Let's make a constant called `MATRICES` that itself is a matrix:\n\n```\nMATRICES = [\n  ['A-L', '4x3'],\n  ['M-X', '3x4'],\n  ['1-8', '2x4'],\n  ['1-144', '12x12'],\n  ['C-J', '4x2'],\n  ['A-Z', '13x2']\n].freeze\n```\n\nWe now need a semi-random way to pick one of these: `range_response, dimensions = MATRICES[rand(6)]`. This will give us a range value as a string like 'C-j' and dimensions like `4x2`. We'll then split that range string up, so we can feed it into our factory:\n```\nrange = range_response.split('-')\nmatrix = Spiralizer::Matrix.the_matrix(range: (range.first..range.last), dimensions: dimensions)\n```\n\n`Thor` wants you to put helper methods for your class in a `no_commands` block, so let's create one of those\nand put all this together so its somewhat presentable. Here's what we have so far:\n\n```\nclass Spiralizer::CLI < Thor\n  attr_reader :matrix\n\n  MATRICES = [\n    ['A-L', '4x3'],\n    ['M-X', '3x4'],\n    ['1-8', '2x4'],\n    ['1-144', '12x12'],\n    ['C-J', '4x2'],\n    ['A-Z', '13x2']\n  ].freeze\n\n  desc 'go', 'starts a prompt that brings users to spiral heaven'\n  def go\n    say \"What would you like to do? (choose a number)\\n 1 - Build a matrix\\n 2 - Generate a random matrix\"\n    action = ask '> '\n    say \"\\n\"\n\n    range, dimensions = random_range_and_dimensions\n    @matrix = Spiralizer::Matrix.the_matrix(range: range, dimensions: dimensions)\n    output_matrix\n  end\n\n  no_commands do\n    def random_range_and_dimensions\n      say \"\\ngenerating...\"\n      sleep 1\n      range_response, dimensions = MATRICES[rand(6)]\n      range = range_response.split('-')\n      return (range.first..range.last), dimensions\n    end\n\n    def output_matrix\n      say \"\\nHere is your M A T R I X\\n------------------------\"\n      matrix.each { |inner| say inner.join(\"\\t\") }\n      exit!\n    end\n  end\nend\n```\n\nYou'll notice that any input we give the prompt at this point will just generate a random matrix from our list. We're making progress!\n\n```\n> bundle exec exe/spiralizer go\n\nWhat would you like to do? (choose a number)\n 1 - Build a matrix\n 2 - Generate a random matrix\n>  2\n\n\ngenerating...\n\nHere is your M A T R I X\n------------------------\n1 2\n3 4\n5 6\n7 8\n```\n\nNext, let's hook up a follow up prompt that asks us if we want to spiralize the matrix. Update your `output_matrix` method:\n\n```\ndef output_matrix\n  say \"\\nHere is your M A T R I X\\n------------------------\"\n  matrix.each { |arr| say arr.join(\"\\t\") }\n\n  say \"\\nWould you like to spiralize it? (choose a number)\\n 1 - Yes\\n 2 - No\"\n  action = ask '> '\n  say \"\\n\"\n  if action == '1'\n    say Spiralizer::Spiralize.new(matrix: matrix).perform\n  end\n\n  exit!\nend\n```\n\nWe are now prompted for more input after the matrix has been generated! Pretty neat.\n\n```\n> bundle exec exe/spiralizer go\n\nWhat would you like to do? (choose a number)\n 1 - Build a matrix\n 2 - Generate a random matrix\n>  2\n\n\ngenerating...\n\nHere is your M A T R I X\n------------------------\nM N O\nP Q R\nS T U\nV W X\n\nWould you like to spiralize it? (choose a number)\n 1 - Yes\n 2 - No\n>  1\n\nm n o r u x w v s p q t\n```\n\nThis is looking good but we still need to handle our first option to allow users to create their own matrix. Let's add a method to handle that input now:\n\n```\n  def user_range_and_dimensions\n    range_response = ask(\"Please provide range. Acceptable format: (A-L) or (1-6)\\n> \")\n    values         = range_response.split('-')\n    dimensions     = ask(\"Please provide dimensions. Acceptable format: (4x3) or (3x2)\\n> \")\n    return (values.first..values.last), dimensions\n  end\n```\n\nWe want this to trigger when the user chooses `'1'` from our introduction prompt so we need to re-work our `go` method:\n\n```\ndef go\n  say \"What would you like to do? (choose a number)\\n 1 - Build a matrix\\n 2 - Generate a random matrix\"\n  action = ask '> '\n  say \"\\n\"\n\n  case action\n  when '1' then range, dimensions = user_range_and_dimensions\n  when '2' then range, dimensions = random_range_and_dimensions\n  else exit!\n  end\n\n  range, dimensions = random_range_and_dimensions\n  @matrix = Spiralizer::Matrix.the_matrix(range: range, dimensions: dimensions)\n  output_matrix\nrescue Spiralizer::InvalidInput => e\n  say \"\\nUh oh! #{e.message}\"\nensure\n  say \"\\nexiting...\"\n  exit!\nend\n```\n\nYou should be getting the hang of how to use `Thor`. Our class is need of some cleanup, but with some refactoring we end up with something like this:\n\n\n```\nrequire 'thor'\nrequire 'spiralizer'\n\nclass Spiralizer::CLI < Thor\n  MATRICES = [\n    ['A-L', '4x3'],\n    ['M-X', '3x4'],\n    ['1-8', '2x4'],\n    ['1-144', '12x12'],\n    ['C-J', '4x2'],\n    ['A-Z', '13x2']\n  ].freeze\n\n  attr_reader :action, :matrix\n\n  desc 'go', 'starts a prompt that brings users to spiral heaven'\n  def go\n    clear_screen!\n    intro_prompt\n    build_matrix\n    output_matrix\n    action_prompt\n    spiralize!\n  rescue Spiralizer::InvalidInput => e\n    say \"\\nUh oh! #{e.message}\"\n  ensure\n    quit_softly\n  end\n\n  no_commands do\n    def intro_prompt\n      say \"What would you like to do? (choose a number)\\n 1 - Build a matrix\\n 2 - Generate a random matrix\"\n      @action = ask '> '\n    end\n\n    def build_matrix\n      say \"\\n\"\n\n      case action\n      when '1' then range, dimensions = user_range_and_dimensions\n      when '2' then range, dimensions = random_range_and_dimensions\n      else quit_softly\n      end\n\n      @matrix = Spiralizer::Matrix.the_matrix(range: range, dimensions: dimensions)\n    end\n\n    def user_range_and_dimensions\n      range_response = ask(\"Please provide range. Acceptable format: (A-L) or (1-6)\\n> \")\n      values         = range_response.split('-')\n      dimensions     = ask(\"Please provide dimensions. Acceptable format: (4x3) or (3x2)\\n> \")\n      return (values.first..values.last), dimensions\n    end\n\n    def random_range_and_dimensions\n      say \"\\ngenerating...\"\n      pause_for_effect\n      range_response, dimensions = MATRICES[rand(6)]\n      range = range_response.split('-')\n      return (range.first..range.last), dimensions\n    end\n\n    def action_prompt\n      say \"\\nWhat would you like to do with it? (choose a number)\\n 1 - Spiralize it\\n 2 - Look at it\"\n      @action = ask '> '\n    end\n\n    def output_matrix\n      say \"\\nHere is your M A T R I X\\n------------------------\"\n      matrix.each { |inner| say inner.join(\"\\t\") }\n    end\n\n    def spiralize!\n      say \"\\n\"\n      say Spiralizer::Spiralize.new(matrix: matrix).perform if action == '1'\n    end\n\n    def clear_screen!\n      return system 'cls' if Gem.win_platform?\n      system 'clear'\n    end\n\n    def pause_for_effect\n      sleep 0.5\n    end\n\n    def quit_softly\n      say \"\\nexiting...\"\n      pause_for_effect\n      exit!\n    end\n  end\nend\n```\n\nNow, let's try it all out:\n\n```\nWhat would you like to do? (choose a number)\n 1 - Build a matrix\n 2 - Generate a random matrix\n>  1\n\nPlease provide range. Acceptable format: (A-L) or (1-6)\n>  1-6\nPlease provide dimensions. Acceptable format: (4x3) or (3x2)\n>  2x3\n\nHere is your M A T R I X\n------------------------\n1 2\n3 4\n5 6\n\nWhat would you like to do with it? (choose a number)\n 1 - Spiralize it\n 2 - Look at it\n>  1\n\n1 2 4 6 5 3\n\nexiting...\n```\n\nOur little gem is complete! There is a lot of cleanup we can and should do, but this is where this blog post ends.\nFeel free to add new functionality and experiment further with Thor. I've gone ahead and added a `Crisscross` class for\nexample in my <a href=\"https://github.com/david-pm/matrix-spiralizer\">repo</a>.\n\nOh! One more thing, if yoy run `bundle exec rake install` bundler will install this gem on your system as a global executable so you can then call it like this:\n```\n> spiralizer go\n```\n\nIf you ever need to uninstall it for some insane reason just run `gem uninstall spiralizer`.\n\nYou can find a finalized version of the code <a href=\"https://github.com/david-pm/matrix-spiralizer\">here</a>.\n\nWell, there you have it. We've built a cli gem from scratch using `Bundler` and `Thor`. I hope this post serves you well and until next time!\n\n\n### (づ。◕‿‿◕。)づ  *:・゚✧ ✧゚・*:・゚✧ ✧゚・",
      "json_metadata": "{\"tags\":[\"technology\",\"ruby\",\"thor\",\"programming\"],\"image\":[\"https://ae01.alicdn.com/kf/HTB1FjN5KFXXXXa6XFXXq6xXFXXXx/1-1-Scale-Full-Metal-Thor-Hammer-Mjolnir-1-1-Replica-Thor-Custom-Cosplay-Hammer-Collection.jpg_640x640.jpg\"],\"links\":[\"https://steemit.com/ruby/@davidpm/create-a-command-line-gem-from-scratch-with-thor\",\"https://github.com/erikhuda/thor\",\"https://github.com/david-pm/matrix-spiralizer\"],\"app\":\"steemit/0.1\",\"format\":\"markdown\"}",
      "parent_author": "",
      "parent_permlink": "ruby",
      "permlink": "create-a-command-line-gem-from-scratch-with-thor-part-two",
      "title": "Create a command-line gem from scratch with Thor (part two)"
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2018-06-03T07:17:48",
  "trx_id": "7f9d967d5f2df769eec5c62c34679339a2ffca2a",
  "trx_in_block": 30,
  "virtual_op": 0
}
2018/06/03 07:16:51
authordavidpm
body![](https://themerkle.com/wp-content/uploads/2017/08/shutterstock_679065259.jpg) If you are a programmer and have been hearing all of the fuss about `THE BLOCKCHAIN` but haven't yet looked into it, and were looking for a starting point, this is the post for you! Feel free to skip the buzzword section and get right to the code if you already understand what a blockchain data structure is ʕ•ᴥ•ʔ ### Buzzword *Blockchain* has become a buzzword that is often used in a way that only confuses newcomers. You'll find definitions in the wild like, "a blockchain is a digital ledger in which transactions made in a cryptocurrency are recorded chronologically and publicly." That is correct, but... do you know what it *is* yet? I didn't. Most often it is used to refer to some platform or decentralized application (dapp), but a blockchain is really just an immutable data structure. Tokens, coins, dapps, etc. are built using this data structure. This data structure is mainly used to distribute a ledger of transactions to every participant in the network. It is a series of records that are linked, or *chained*, together. What these records store is transaction data. The transactions are verifiable by anybody who has the data and/or the application to use that data, due to the way the data structure works. So, the word itself changes meaning based on context, whether it be the micro data structure or the macro network of decentralized ledgers. The data structure itself is the basic unit of all of this crypto-madness and that is the context of this post. ### Building blocks It helps me to flesh out abstract concepts by taking something apart and putting it back together. In doing so with a few repos that leverage a blockchain data structure, I thought it would prove useful to others if I made a simple one in Ruby. Hopefully, this post will help reify the concept of a blockchain through code. Let's create a `Block` (this will all be done in [Ruby](https://www.ruby-lang.org/en/), but you shouldn't have any problem following along regardless of your programming background): ``` class Block attr_reader :index, :parent, :created_at, :data, :hash def initialize(index:, data:, parent: '') @index = index.to_i @data = data @parent = parent.empty? ? '0' : parent @created_at = Time.now.to_s @hash = '' end end ``` A `Block` is just something that stores data and some essential metadata. It's really that simple. Our `Block` class contains some `data`, an `index`, a reference to a `parent` block, a `created_at` date, and a `hash`. These properties will all be instantiated whenever we create a new block object. A key component to these blocks, and something we've purposefully glossed over to this point, is their immutability. Every block gets a cryptographic hash applied to it. Every protocol has its own way of applying this hash, but we're not so interested in how it gets applied in our little proof-of-concept. We're just gonna use Ruby's standard [Digest](https://ruby-doc.org/stdlib-2.5.0/libdoc/digest/rdoc/Digest/SHA2.html) class and slap a hash onto a block when it gets instantiated. ``` class Block ... def initialize(index:, data:, parent: '') ... @hash = hashsum end private def hashsum hashable = "#{index.to_s} #{parent} #{created_at} #{data.to_s}" Digest::SHA2.new(256).hexdigest(hashable) end end ``` `hashsum` is just a private method that takes the index from the block currently being created, the parent hash, the created at date, and data and feed that to Ruby's `Digest::SHA2(256).hexdigest`. Trying it out yields results that look like this: ``` (main)> Block.new(index: 0, data: 'your mom') => #<Block:0x00007ff6c4cf1f80 @created_at="2018-05-28 17:33:03 -0700", @data="your mom", @hash="ae8f611206e621c3a50cdb695c58adda78b3432986ed0f8dd4e1246456c332a2", @index=0, @parent="0"> (main)> Block.new(index: 1, data: 'your dad', parent: _.hash) => #<Block:0x00007ff6c477c5f0 @created_at="2018-05-28 17:34:27 -0700", @data="your dad", @hash="981acd98146a22d724524df236fb57161dec013c9820d683d098d5c05880852e", @index=1, @parent="ae8f611206e621c3a50cdb695c58adda78b3432986ed0f8dd4e1246456c332a2"> ``` We can now create blocks! What's more our blocks know about each other. Notice that our second block stores a reference to its parent block by storing the parent's hash value. Now we just need a convenient wrapper to chain them altogether, as well as provide some accessors and validators. We'll do this by creating a new class `Blockchain` which will store a series of blocks: ``` class Blockchain def initialize @chain = [] end end ``` Simple enough, but we'll want an easy way to push new blocks into this chain. Let's add a method that will take arbitrary data for storage, calculate the position of the newly created block, create it and push it onto the blockchain. This isn't very [SOLID](https://en.wikipedia.org/wiki/SOLID) code but we're hurrying to finish our proof-of-concept (PoC) and can always refactor this later. ``` class Blockchain ... # not very SOLID but this just a PoC def add_block(data) chain << Block.new(index: chain.size, data: data, parent: last_hash) end def last_hash chain.last&.hash || '' end private attr_reader :chain end ``` Trying it out in `pry` works like a charm: ``` (main)> load 'blockchain.rb' => true (main)> bc = Blockchain.new => #<Blockchain:0x00007fe248878638 @chain=[]> (main)> bc.add_block({ start_over: true }) => [#<Block:0x00007fe248bc8770 @created_at="2018-05-28 17:51:44 -0700", @data={:start_over=>true}, @hash="a52c05c50d0a024acbb965f5d7f78fddcd54166b7154dfc487fd2f54d428a208", @index=0, @parent="0">] (main)> bc.add_block({ start_over: false }) => [#<Block:0x00007fe248bc8770 @created_at="2018-05-28 17:51:44 -0700", @data={:start_over=>true}, @hash="a52c05c50d0a024acbb965f5d7f78fddcd54166b7154dfc487fd2f54d428a208", @index=0, @parent="0">, #<Block:0x00007fe245635950 @created_at="2018-05-28 17:51:51 -0700", @data={:start_over=>false}, @hash="a159d1e698518af46f82938c2f7e830a80d16e527a1e667fdd298417004f525e", @index=1, @parent="a52c05c50d0a024acbb965f5d7f78fddcd54166b7154dfc487fd2f54d428a208">] ``` Et voila! The meat of our blockchain is now implemented! No blockchain would be complete without the ability to validate its integrity. So, let's add a quick valid? method which will loop thru all the blocks on the chain and make sure the hash values match and that each child has the proper parent. If this method ever fails, our structure's immutability has been compromised and is a very bad thing. ``` class Blockchain ... def valid? return true if chain.size <= 1 link = chain.each # coerce to an enumerator loop do # loops rescue StopIteration errors previous = link.next return false if previous.hash != link.next.parent end return true end def last chain.last end def size chain.size end ... end ``` With this simple validator in place we really only have some a few convenient frills to add. I'd like a welcome message and a base block to be instantiated whenever a new blockchain object is created. Let's take care of that now to wrap this up: ``` class Blockchain def initialize welcome @chain = [] add_first_block! end ... private ... def add_first_block! chain << Block.new(index: 0, data: 'Genenis Block') end def welcome puts <<~BLCKCHN Welcome, to Blockchain! To add to the chain, call `#add_block` and pass in a data\n\n BLCKCHN end end ``` We have created two private methods that will be invoked whenever a new blockchain is created (in the real world, your blockchain should only every be created one time). We'll print a welcome message to the screen and create the first block of the chain. There you have it, a simple but functional blockchain data structure! The [finished product](https://gist.github.com/david-pm/4791fbb0aa2a0cf4efce9adb044352d3) is well under one hundred lines of code. This is by no means ready for any kind of production application. You will want additional safe guards against tampering, and robust authentication etc. but I hope this has been a helpful introduction to how this data structure works. With thousands of applications already in use the blockchain isn't going away, but as you can see it is really nothing to be afraid of and isn't even terribly novel. Until next time! ### ༼ ºل͟º ༼ ºل͟º ༼ ºل͟º ༽ ºل͟º ༽ ºل͟º ༽
json metadata{"tags":["blockchain","technology","ruby","crypto","programming"],"image":["https://themerkle.com/wp-content/uploads/2017/08/shutterstock_679065259.jpg"],"links":["https://www.ruby-lang.org/en/","https://ruby-doc.org/stdlib-2.5.0/libdoc/digest/rdoc/Digest/SHA2.html","https://en.wikipedia.org/wiki/SOLID","https://gist.github.com/david-pm/4791fbb0aa2a0cf4efce9adb044352d3"],"app":"steemit/0.1","format":"markdown"}
parent author
parent permlinkblockchain
permlinkblockchain-proof-of-concept
titleBlockchain Proof of Concept
Transaction InfoBlock #22992156/Trx 87b5cf3ad14463288b130d51b077043019b10ccb
View Raw JSON Data
{
  "block": 22992156,
  "op": [
    "comment",
    {
      "author": "davidpm",
      "body": "![](https://themerkle.com/wp-content/uploads/2017/08/shutterstock_679065259.jpg)\n\nIf you are a programmer and have been hearing all of the fuss about `THE BLOCKCHAIN` but haven't yet looked into it, and were looking for a starting point, this is the post for you!  Feel free to skip the buzzword section and get right to the code if you already understand what a blockchain data structure is ʕ•ᴥ•ʔ\n\n### Buzzword\n\n*Blockchain* has become a buzzword that is often used in a way that only confuses newcomers. You'll find definitions in the wild like, \"a blockchain is a digital ledger in which transactions made in a cryptocurrency are recorded chronologically and publicly.\" That is correct, but... do you know what it *is* yet? I didn't.\n\nMost often it is used to refer to some platform or decentralized application (dapp), but a blockchain is really just an immutable data structure. Tokens, coins, dapps, etc. are built using this data structure. This data structure is mainly used to distribute a ledger of transactions to every participant in the network. It is a series of records that are linked, or *chained*, together. What these records store is transaction data. The transactions are verifiable by anybody who has the data and/or the application to use that data, due to the way the data structure works.\n\nSo, the word itself changes meaning based on context, whether it be the micro data structure or the macro network of decentralized ledgers. The data structure itself is the basic unit of all of this crypto-madness and that is the context of this post.\n\n### Building blocks\n\nIt helps me to flesh out abstract concepts by taking something apart and putting it back together. In doing so with a few repos that leverage a blockchain data structure, I thought it would prove useful to others if I made a simple one in Ruby. Hopefully, this post will help reify the concept of a blockchain through code. \n\nLet's create a `Block` (this will all be done in [Ruby](https://www.ruby-lang.org/en/), but you shouldn't have any problem following along regardless of your programming background):\n\n```\nclass Block\n  attr_reader :index, :parent, :created_at, :data, :hash\n\n  def initialize(index:, data:, parent: '')\n    @index         = index.to_i\n    @data          = data\n    @parent        = parent.empty? ? '0' : parent\n    @created_at    = Time.now.to_s\n    @hash          = ''\n  end\nend\n\n```\n\nA `Block` is just something that stores data and some essential metadata. It's really that simple. Our `Block` class contains some `data`, an `index`, a reference to a `parent` block, a `created_at` date, and a `hash`. These properties will all be instantiated whenever we create a new block object.\n\nA key component to these blocks, and something we've purposefully glossed over to this point, is their immutability. Every block gets a cryptographic hash applied to it. Every protocol has its own way of applying this hash, but we're not so interested in how it gets applied in our little proof-of-concept. We're just gonna use Ruby's standard [Digest](https://ruby-doc.org/stdlib-2.5.0/libdoc/digest/rdoc/Digest/SHA2.html) class and slap a hash onto a block when it gets instantiated.\n\n```\nclass Block\n  ...\n  def initialize(index:, data:, parent: '')\n     ...\n    @hash          = hashsum\n  end\n\n  private\n\n  def hashsum\n    hashable = \"#{index.to_s} #{parent} #{created_at} #{data.to_s}\"\n    Digest::SHA2.new(256).hexdigest(hashable)\n  end\nend\n```\n\n`hashsum` is just a private method that takes the index from the block currently being created, the parent hash, the created at date, and data and feed that to Ruby's `Digest::SHA2(256).hexdigest`. Trying it out yields results that look like this:\n\n```\n(main)> Block.new(index: 0, data: 'your mom')\n=> #<Block:0x00007ff6c4cf1f80\n @created_at=\"2018-05-28 17:33:03 -0700\",\n @data=\"your mom\",\n @hash=\"ae8f611206e621c3a50cdb695c58adda78b3432986ed0f8dd4e1246456c332a2\",\n @index=0,\n @parent=\"0\">\n(main)> Block.new(index: 1, data: 'your dad', parent: _.hash)\n=> #<Block:0x00007ff6c477c5f0\n @created_at=\"2018-05-28 17:34:27 -0700\",\n @data=\"your dad\",\n @hash=\"981acd98146a22d724524df236fb57161dec013c9820d683d098d5c05880852e\",\n @index=1,\n @parent=\"ae8f611206e621c3a50cdb695c58adda78b3432986ed0f8dd4e1246456c332a2\">\n```\n\nWe can now create blocks! What's more our blocks know about each other. Notice that our second block stores a reference to its parent block by storing the parent's hash value. Now we just need a convenient wrapper to chain them altogether, as well as provide some accessors and validators. \n\nWe'll do this by creating a new class `Blockchain` which will store a series of blocks:\n\n```\nclass Blockchain\n  def initialize\n     @chain = []\n  end   \nend\n```\n\nSimple enough, but we'll want an easy way to push new blocks into this chain. Let's add a method that will take arbitrary data for storage, calculate the position of the newly created block, create it and push it onto the blockchain. This isn't very [SOLID](https://en.wikipedia.org/wiki/SOLID) code but we're hurrying to finish our proof-of-concept (PoC) and can always refactor this later.\n\n\n```\nclass Blockchain\n  ...\n  # not very SOLID but this just a PoC\n  def add_block(data)\n    chain << Block.new(index: chain.size, data: data, parent: last_hash)\n  end\n\n  def last_hash\n    chain.last&.hash || ''\n  end\n\n  private\n\n  attr_reader :chain\nend\n```\n\nTrying it out in `pry` works like a charm:\n\n\n```\n(main)> load 'blockchain.rb'\n=> true\n(main)> bc = Blockchain.new\n=> #<Blockchain:0x00007fe248878638 @chain=[]>\n(main)> bc.add_block({ start_over: true })\n=> [#<Block:0x00007fe248bc8770\n  @created_at=\"2018-05-28 17:51:44 -0700\",\n  @data={:start_over=>true},\n  @hash=\"a52c05c50d0a024acbb965f5d7f78fddcd54166b7154dfc487fd2f54d428a208\",\n  @index=0,\n  @parent=\"0\">]\n(main)> bc.add_block({ start_over: false })\n=> [#<Block:0x00007fe248bc8770\n  @created_at=\"2018-05-28 17:51:44 -0700\",\n  @data={:start_over=>true},\n  @hash=\"a52c05c50d0a024acbb965f5d7f78fddcd54166b7154dfc487fd2f54d428a208\",\n  @index=0,\n  @parent=\"0\">,\n #<Block:0x00007fe245635950\n  @created_at=\"2018-05-28 17:51:51 -0700\",\n  @data={:start_over=>false},\n  @hash=\"a159d1e698518af46f82938c2f7e830a80d16e527a1e667fdd298417004f525e\",\n  @index=1,\n  @parent=\"a52c05c50d0a024acbb965f5d7f78fddcd54166b7154dfc487fd2f54d428a208\">]\n\n```\n\nEt voila! The meat of our blockchain is now implemented! No blockchain would be complete without the ability to validate its integrity. So, let's add a quick valid? method which will loop thru all the blocks on the chain and make sure the hash values match and that each child has the proper parent. If this method ever fails, our structure's immutability has been compromised and is a very bad thing.\n\n```\nclass Blockchain\n  ...\n  def valid?\n    return true if chain.size <= 1\n    link = chain.each # coerce to an enumerator\n    loop do           # loops rescue StopIteration errors\n      previous = link.next\n      return false if previous.hash != link.next.parent\n    end\n    return true\n  end\n\n  def last\n    chain.last\n  end\n\n  def size\n    chain.size\n  end\n...\nend\n```\n\nWith this simple validator in place we really only have some a few convenient frills to add. I'd like a welcome message and a base block to be instantiated whenever a new blockchain object is created. Let's take care of that now to wrap this up:\n\n```\nclass Blockchain\n  def initialize\n    welcome\n    @chain = []\n    add_first_block!\n  end\n\n  ...\n  private\n  ...\n  def add_first_block!\n    chain << Block.new(index: 0, data: 'Genenis Block')\n  end\n\n  def welcome\n    puts <<~BLCKCHN\n      Welcome, to Blockchain!\n      To add to the chain, call `#add_block` and pass in a data\\n\\n\n    BLCKCHN\n  end\nend\n```\n\nWe have created two private methods that will be invoked whenever a new blockchain is created (in the real world, your blockchain should only every be created one time). We'll print a welcome message to the screen and create the first block of the chain. There you have it, a simple but functional blockchain data structure! The [finished product](https://gist.github.com/david-pm/4791fbb0aa2a0cf4efce9adb044352d3) is well under one hundred lines of code.\n\nThis is by no means ready for any kind of production application. You will want additional safe guards against tampering, and robust authentication etc. but I hope this has been a helpful introduction to how this data structure works. With thousands of applications already in use the blockchain isn't going away, but as you can see it is really nothing to be afraid of and isn't even terribly novel. Until next time!\n\n### ༼ ºل͟º ༼ ºل͟º ༼ ºل͟º ༽ ºل͟º ༽ ºل͟º ༽",
      "json_metadata": "{\"tags\":[\"blockchain\",\"technology\",\"ruby\",\"crypto\",\"programming\"],\"image\":[\"https://themerkle.com/wp-content/uploads/2017/08/shutterstock_679065259.jpg\"],\"links\":[\"https://www.ruby-lang.org/en/\",\"https://ruby-doc.org/stdlib-2.5.0/libdoc/digest/rdoc/Digest/SHA2.html\",\"https://en.wikipedia.org/wiki/SOLID\",\"https://gist.github.com/david-pm/4791fbb0aa2a0cf4efce9adb044352d3\"],\"app\":\"steemit/0.1\",\"format\":\"markdown\"}",
      "parent_author": "",
      "parent_permlink": "blockchain",
      "permlink": "blockchain-proof-of-concept",
      "title": "Blockchain Proof of Concept"
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2018-06-03T07:16:51",
  "trx_id": "87b5cf3ad14463288b130d51b077043019b10ccb",
  "trx_in_block": 34,
  "virtual_op": 0
}
2018/06/03 07:15:30
authorarmacener
permlinkdaily-dose-of-seinfeld-03-06-2018
voterdavidpm
weight10000 (100.00%)
Transaction InfoBlock #22992129/Trx 2331e3a6a636642dc53d558548e8b044058bded0
View Raw JSON Data
{
  "block": 22992129,
  "op": [
    "vote",
    {
      "author": "armacener",
      "permlink": "daily-dose-of-seinfeld-03-06-2018",
      "voter": "davidpm",
      "weight": 10000
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2018-06-03T07:15:30",
  "trx_id": "2331e3a6a636642dc53d558548e8b044058bded0",
  "trx_in_block": 7,
  "virtual_op": 0
}
2018/06/03 06:59:09
authordavidpm
body@@ -205,16 +205,17 @@ here%3C/a%3E +. %0A%0A---%0A%0AL
json metadata{"tags":["ruby","gem","thor","cli","matrix"],"image":["https://media.giphy.com/media/S4HKH9KgRGMdq/giphy.gif","https://upload.wikimedia.org/wikipedia/commons/3/32/Chrysopoea_of_Cleopatra_1.png"],"links":["https://steemit.com/ruby/@davidpm/create-a-command-line-gem-from-scratch-with-thor-part-two","https://bundler.io/","https://git-scm.com/","http://rspec.info/","https://github.com/ruby/rake","https://github.com/erikhuda/thor","https://github.com/bundler/bundler/blob/master/lib/bundler/dsl.rb#L58","https://en.wikipedia.org/wiki/Ouroboros"],"app":"steemit/0.1","format":"markdown"}
parent author
parent permlinkruby
permlinkcreate-a-command-line-gem-from-scratch-with-thor
titleCreate a command-line gem from scratch with Thor
Transaction InfoBlock #22991802/Trx 0de75e7e45d6812f3fe9196e5a65b1d4ce7b3461
View Raw JSON Data
{
  "block": 22991802,
  "op": [
    "comment",
    {
      "author": "davidpm",
      "body": "@@ -205,16 +205,17 @@\n here%3C/a%3E\n+.\n %0A%0A---%0A%0AL\n",
      "json_metadata": "{\"tags\":[\"ruby\",\"gem\",\"thor\",\"cli\",\"matrix\"],\"image\":[\"https://media.giphy.com/media/S4HKH9KgRGMdq/giphy.gif\",\"https://upload.wikimedia.org/wikipedia/commons/3/32/Chrysopoea_of_Cleopatra_1.png\"],\"links\":[\"https://steemit.com/ruby/@davidpm/create-a-command-line-gem-from-scratch-with-thor-part-two\",\"https://bundler.io/\",\"https://git-scm.com/\",\"http://rspec.info/\",\"https://github.com/ruby/rake\",\"https://github.com/erikhuda/thor\",\"https://github.com/bundler/bundler/blob/master/lib/bundler/dsl.rb#L58\",\"https://en.wikipedia.org/wiki/Ouroboros\"],\"app\":\"steemit/0.1\",\"format\":\"markdown\"}",
      "parent_author": "",
      "parent_permlink": "ruby",
      "permlink": "create-a-command-line-gem-from-scratch-with-thor",
      "title": "Create a command-line gem from scratch with Thor"
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2018-06-03T06:59:09",
  "trx_id": "0de75e7e45d6812f3fe9196e5a65b1d4ce7b3461",
  "trx_in_block": 13,
  "virtual_op": 0
}
2018/06/03 06:58:51
authordavidpm
body@@ -95,16 +95,17 @@ o %3Ca hre +f ='https:
json metadata{"tags":["ruby","gem","thor","cli","matrix"],"image":["https://media.giphy.com/media/S4HKH9KgRGMdq/giphy.gif","https://upload.wikimedia.org/wikipedia/commons/3/32/Chrysopoea_of_Cleopatra_1.png"],"links":["https://steemit.com/ruby/@davidpm/create-a-command-line-gem-from-scratch-with-thor-part-two","https://bundler.io/","https://git-scm.com/","http://rspec.info/","https://github.com/ruby/rake","https://github.com/erikhuda/thor","https://github.com/bundler/bundler/blob/master/lib/bundler/dsl.rb#L58","https://en.wikipedia.org/wiki/Ouroboros"],"app":"steemit/0.1","format":"markdown"}
parent author
parent permlinkruby
permlinkcreate-a-command-line-gem-from-scratch-with-thor
titleCreate a command-line gem from scratch with Thor
Transaction InfoBlock #22991796/Trx 108f2e890fdbffa2aa0e143c7f104e4866f5679b
View Raw JSON Data
{
  "block": 22991796,
  "op": [
    "comment",
    {
      "author": "davidpm",
      "body": "@@ -95,16 +95,17 @@\n o %3Ca hre\n+f\n ='https:\n",
      "json_metadata": "{\"tags\":[\"ruby\",\"gem\",\"thor\",\"cli\",\"matrix\"],\"image\":[\"https://media.giphy.com/media/S4HKH9KgRGMdq/giphy.gif\",\"https://upload.wikimedia.org/wikipedia/commons/3/32/Chrysopoea_of_Cleopatra_1.png\"],\"links\":[\"https://steemit.com/ruby/@davidpm/create-a-command-line-gem-from-scratch-with-thor-part-two\",\"https://bundler.io/\",\"https://git-scm.com/\",\"http://rspec.info/\",\"https://github.com/ruby/rake\",\"https://github.com/erikhuda/thor\",\"https://github.com/bundler/bundler/blob/master/lib/bundler/dsl.rb#L58\",\"https://en.wikipedia.org/wiki/Ouroboros\"],\"app\":\"steemit/0.1\",\"format\":\"markdown\"}",
      "parent_author": "",
      "parent_permlink": "ruby",
      "permlink": "create-a-command-line-gem-from-scratch-with-thor",
      "title": "Create a command-line gem from scratch with Thor"
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2018-06-03T06:58:51",
  "trx_id": "108f2e890fdbffa2aa0e143c7f104e4866f5679b",
  "trx_in_block": 31,
  "virtual_op": 0
}
2018/06/03 06:57:42
authordavidpm
body@@ -79,16 +79,141 @@ scratch. + Part two %3Ca hre='https://steemit.com/ruby/@davidpm/create-a-command-line-gem-from-scratch-with-thor-part-two'%3Efound here%3C/a%3E %0A%0A---%0A%0AL
json metadata{"tags":["ruby","gem","thor","cli","matrix"],"image":["https://media.giphy.com/media/S4HKH9KgRGMdq/giphy.gif","https://upload.wikimedia.org/wikipedia/commons/3/32/Chrysopoea_of_Cleopatra_1.png"],"links":["https://bundler.io/","https://git-scm.com/","http://rspec.info/","https://github.com/ruby/rake","https://github.com/erikhuda/thor","https://github.com/bundler/bundler/blob/master/lib/bundler/dsl.rb#L58","https://en.wikipedia.org/wiki/Ouroboros"],"app":"steemit/0.1","format":"markdown"}
parent author
parent permlinkruby
permlinkcreate-a-command-line-gem-from-scratch-with-thor
titleCreate a command-line gem from scratch with Thor
Transaction InfoBlock #22991773/Trx e35eb45ba5b804d83e03f009f5e275dee7c0d4bd
View Raw JSON Data
{
  "block": 22991773,
  "op": [
    "comment",
    {
      "author": "davidpm",
      "body": "@@ -79,16 +79,141 @@\n scratch.\n+ Part two %3Ca hre='https://steemit.com/ruby/@davidpm/create-a-command-line-gem-from-scratch-with-thor-part-two'%3Efound here%3C/a%3E\n %0A%0A---%0A%0AL\n",
      "json_metadata": "{\"tags\":[\"ruby\",\"gem\",\"thor\",\"cli\",\"matrix\"],\"image\":[\"https://media.giphy.com/media/S4HKH9KgRGMdq/giphy.gif\",\"https://upload.wikimedia.org/wikipedia/commons/3/32/Chrysopoea_of_Cleopatra_1.png\"],\"links\":[\"https://bundler.io/\",\"https://git-scm.com/\",\"http://rspec.info/\",\"https://github.com/ruby/rake\",\"https://github.com/erikhuda/thor\",\"https://github.com/bundler/bundler/blob/master/lib/bundler/dsl.rb#L58\",\"https://en.wikipedia.org/wiki/Ouroboros\"],\"app\":\"steemit/0.1\",\"format\":\"markdown\"}",
      "parent_author": "",
      "parent_permlink": "ruby",
      "permlink": "create-a-command-line-gem-from-scratch-with-thor",
      "title": "Create a command-line gem from scratch with Thor"
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2018-06-03T06:57:42",
  "trx_id": "e35eb45ba5b804d83e03f009f5e275dee7c0d4bd",
  "trx_in_block": 6,
  "virtual_op": 0
}
2018/06/03 06:55:54
authordavidpm
bodyThis is the last in a two-part post on creating a Ruby cli gem from scratch. Part one <a href='https://steemit.com/ruby/@davidpm/create-a-command-line-gem-from-scratch-with-thor'>found here</a>. --- Picking up from our <a href='https://steemit.com/ruby/@davidpm/create-a-command-line-gem-from-scratch-with-thor'>last post</a>, we want to start working on our user interface. In this case we want a cli that looks something like this: ``` #-> what would you like to do? 1 - Build a matrix 2 - Generate a random matrix #-> now that we have a matrix, would you like to spiralize it? 1 - Spiralize it 2 - Stare at it ``` ### Thor ![](https://ae01.alicdn.com/kf/HTB1FjN5KFXXXXa6XFXXq6xXFXXXx/1-1-Scale-Full-Metal-Thor-Hammer-Mjolnir-1-1-Replica-Thor-Custom-Cosplay-Hammer-Collection.jpg_640x640.jpg) We're going to use <a href='https://github.com/erikhuda/thor'>Thor</a>, which is a toolkit for building command-line tools and applications. In fact, `Bundler` itself was written using `Thor`. Let's add it to the bottom of our `gemspec` and re-install: ``` # spiralizer.gemspec spec.add_dependency "thor" --- # from cmd line: > bundle install ``` Now, we just need to require it in and start using it! ``` require 'thor' ... ``` But, this is also a good time to start separating our code a bit, so we don't let things get to messy. So, let's create a file called `lib/spiralizer/cli.rb`, and move our `require` statement into this file. Then we will stub out a class under the `Spiralizer` namespace and inherit from `Thor`: ``` require 'thor' require 'spiralizer' class Spiralizer::CLI < Thor end ``` Back in our main module we will need to require in this new file: ``` require "spiralizer/version" require "spiralizer/cli" module Spiralizer ... end ``` Our cli is gem that users can install on their systems. We want them to have an executable that will take a simple command to spin up prompt we defined earlier: Say the user enters `spiralizer go` for example. We would want output similar to what we envisioned above: ``` $ >spiralizer go #-> what would you like to do? 1 - Build a matrix 2 - Generate a random matrix ``` Thor gives us some easy-to-use tooling to get this thing going in no time. We'll get started by defining our `go` method, and use the `desc` macro annotation get some magic documentation: ``` require 'thor' require 'spiralizer' class Spiralizer::CLI < Thor desc 'go', 'starts a prompt that brings users to spiral heaven' def go say "What would you like to do? (choose a number)\n 1 - Build a matrix\n 2 - Generate a random matrix" action = ask '> ' say "\nyou picked #{action}" exit! end end ``` `Thor` provides some intuitive methods like `say` to output something to the user, and `ask` to prompt the user for input. They work just as expected. Notice that you can assign the result of `ask` to a variable. To start playing around with it, we just need to create a new directory called `exe` (alternatively, we could have scaffolded our gem with a `--exe` option but here we are). It should be a sibling directory of `lib` and `bin`. `cd exe && touch spiralizer` to create a file. Then, give it executable permissions: `> chmod +x exe/spiralizer`, and dump this inside: ``` #!/usr/bin/env ruby require 'spiralizer/cli' Spiralizer::CLI.start ``` And let's give 'er a whirl: ``` > bundle exec exe/spiralizer Commands: spiralizer go # starts a prompt that brings users to spiral heaven spiralizer help [COMMAND] # Describe available commands or one specific command ``` Notice the helpful output Thor gives us. We have two commands available: `go` and `help`. Let's try them out: ``` > bundle exec exe/spiralizer help Commands: spiralizer go # starts a prompt that brings users to spiral heaven spiralizer help [COMMAND] # Describe available commands or one specific command > bundle exec exe/spiralizer go What would you like to do? (choose a number) 1 - Build a matrix 2 - Generate a random matrix > 1 you picked 1 ``` So, `help` is the default action, and we can see our new `go` command is registered properly. Right now our prompt gives us two options, let's add functionality for our second option. When a user enters `'2'` we want to spit out a random matrix. We can do a quick and dirty version of this and just hard code some matrix range/dimension pairs that we will feed to our matrix factory. Let's make a constant called `MATRICES` that itself is a matrix: ``` MATRICES = [ ['A-L', '4x3'], ['M-X', '3x4'], ['1-8', '2x4'], ['1-144', '12x12'], ['C-J', '4x2'], ['A-Z', '13x2'] ].freeze ``` We now need a semi-random way to pick one of these: `range_response, dimensions = MATRICES[rand(6)]`. This will give us a range value as a string like 'C-j' and dimensions like `4x2`. We'll then split that range string up, so we can feed it into our factory: ``` range = range_response.split('-') matrix = Spiralizer::Matrix.the_matrix(range: (range.first..range.last), dimensions: dimensions) ``` `Thor` wants you to put helper methods for your class in a `no_commands` block, so let's create one of those and put all this together so its somewhat presentable. Here's what we have so far: ``` class Spiralizer::CLI < Thor attr_reader :matrix MATRICES = [ ['A-L', '4x3'], ['M-X', '3x4'], ['1-8', '2x4'], ['1-144', '12x12'], ['C-J', '4x2'], ['A-Z', '13x2'] ].freeze desc 'go', 'starts a prompt that brings users to spiral heaven' def go say "What would you like to do? (choose a number)\n 1 - Build a matrix\n 2 - Generate a random matrix" action = ask '> ' say "\n" range, dimensions = random_range_and_dimensions @matrix = Spiralizer::Matrix.the_matrix(range: range, dimensions: dimensions) output_matrix end no_commands do def random_range_and_dimensions say "\ngenerating..." sleep 1 range_response, dimensions = MATRICES[rand(6)] range = range_response.split('-') return (range.first..range.last), dimensions end def output_matrix say "\nHere is your M A T R I X\n------------------------" matrix.each { |inner| say inner.join("\t") } exit! end end end ``` You'll notice that any input we give the prompt at this point will just generate a random matrix from our list. We're making progress! ``` > bundle exec exe/spiralizer go What would you like to do? (choose a number) 1 - Build a matrix 2 - Generate a random matrix > 2 generating... Here is your M A T R I X ------------------------ 1 2 3 4 5 6 7 8 ``` Next, let's hook up a follow up prompt that asks us if we want to spiralize the matrix. Update your `output_matrix` method: ``` def output_matrix say "\nHere is your M A T R I X\n------------------------" matrix.each { |arr| say arr.join("\t") } say "\nWould you like to spiralize it? (choose a number)\n 1 - Yes\n 2 - No" action = ask '> ' say "\n" if action == '1' say Spiralizer::Spiralize.new(matrix: matrix).perform end exit! end ``` We are now prompted for more input after the matrix has been generated! Pretty neat. ``` > bundle exec exe/spiralizer go What would you like to do? (choose a number) 1 - Build a matrix 2 - Generate a random matrix > 2 generating... Here is your M A T R I X ------------------------ M N O P Q R S T U V W X Would you like to spiralize it? (choose a number) 1 - Yes 2 - No > 1 m n o r u x w v s p q t ``` This is looking good but we still need to handle our first option to allow users to create their own matrix. Let's add a method to handle that input now: ``` def user_range_and_dimensions range_response = ask("Please provide range. Acceptable format: (A-L) or (1-6)\n> ") values = range_response.split('-') dimensions = ask("Please provide dimensions. Acceptable format: (4x3) or (3x2)\n> ") return (values.first..values.last), dimensions end ``` We want this to trigger when the user chooses `'1'` from our introduction prompt so we need to re-work our `go` method: ``` def go say "What would you like to do? (choose a number)\n 1 - Build a matrix\n 2 - Generate a random matrix" action = ask '> ' say "\n" case action when '1' then range, dimensions = user_range_and_dimensions when '2' then range, dimensions = random_range_and_dimensions else exit! end range, dimensions = random_range_and_dimensions @matrix = Spiralizer::Matrix.the_matrix(range: range, dimensions: dimensions) output_matrix rescue Spiralizer::InvalidInput => e say "\nUh oh! #{e.message}" ensure say "\nexiting..." exit! end ``` You should be getting the hang of how to use `Thor`. Our class is need of some cleanup, but with some refactoring we end up with something like this: ``` require 'thor' require 'spiralizer' class Spiralizer::CLI < Thor MATRICES = [ ['A-L', '4x3'], ['M-X', '3x4'], ['1-8', '2x4'], ['1-144', '12x12'], ['C-J', '4x2'], ['A-Z', '13x2'] ].freeze attr_reader :action, :matrix desc 'go', 'starts a prompt that brings users to spiral heaven' def go clear_screen! intro_prompt build_matrix output_matrix action_prompt spiralize! rescue Spiralizer::InvalidInput => e say "\nUh oh! #{e.message}" ensure quit_softly end no_commands do def intro_prompt say "What would you like to do? (choose a number)\n 1 - Build a matrix\n 2 - Generate a random matrix" @action = ask '> ' end def build_matrix say "\n" case action when '1' then range, dimensions = user_range_and_dimensions when '2' then range, dimensions = random_range_and_dimensions else quit_softly end @matrix = Spiralizer::Matrix.the_matrix(range: range, dimensions: dimensions) end def user_range_and_dimensions range_response = ask("Please provide range. Acceptable format: (A-L) or (1-6)\n> ") values = range_response.split('-') dimensions = ask("Please provide dimensions. Acceptable format: (4x3) or (3x2)\n> ") return (values.first..values.last), dimensions end def random_range_and_dimensions say "\ngenerating..." pause_for_effect range_response, dimensions = MATRICES[rand(6)] range = range_response.split('-') return (range.first..range.last), dimensions end def action_prompt say "\nWhat would you like to do with it? (choose a number)\n 1 - Spiralize it\n 2 - Look at it" @action = ask '> ' end def output_matrix say "\nHere is your M A T R I X\n------------------------" matrix.each { |inner| say inner.join("\t") } end def spiralize! say "\n" say Spiralizer::Spiralize.new(matrix: matrix).perform if action == '1' end def clear_screen! return system 'cls' if Gem.win_platform? system 'clear' end def pause_for_effect sleep 0.5 end def quit_softly say "\nexiting..." pause_for_effect exit! end end end ``` Now, let's try it all out: ``` What would you like to do? (choose a number) 1 - Build a matrix 2 - Generate a random matrix > 1 Please provide range. Acceptable format: (A-L) or (1-6) > 1-6 Please provide dimensions. Acceptable format: (4x3) or (3x2) > 2x3 Here is your M A T R I X ------------------------ 1 2 3 4 5 6 What would you like to do with it? (choose a number) 1 - Spiralize it 2 - Look at it > 1 1 2 4 6 5 3 exiting... ``` Our little gem is complete! There is a lot of cleanup we can and should do, but this is where this blog post ends. Feel free to add new functionality and experiment further with Thor. I've gone ahead and added a `Crisscross` class for example in my <a href="https://github.com/david-pm/matrix-spiralizer">repo</a>. Oh! One more thing, if yoy run `bundle exec rake install` bundler will install this gem on your system as a global executable so you can then call it like this: ``` > spiralizer go ``` If you ever need to uninstall it for some insane reason just run `gem uninstall spiralizer`. You can find a finalized version of the code <a href="https://github.com/david-pm/matrix-spiralizer">here</a>. Well, there you have it. We've built a cli gem from scratch using `Bundler` and `Thor`. I hope this post serves you well and until next time! ### (づ。◕‿‿◕。)づ *:・゚✧ ✧゚・*:・゚✧ ✧゚・
json metadata{"tags":["ruby","thor","gem","cli","programming"],"image":["https://ae01.alicdn.com/kf/HTB1FjN5KFXXXXa6XFXXq6xXFXXXx/1-1-Scale-Full-Metal-Thor-Hammer-Mjolnir-1-1-Replica-Thor-Custom-Cosplay-Hammer-Collection.jpg_640x640.jpg"],"links":["https://steemit.com/ruby/@davidpm/create-a-command-line-gem-from-scratch-with-thor","https://github.com/erikhuda/thor","https://github.com/david-pm/matrix-spiralizer"],"app":"steemit/0.1","format":"markdown"}
parent author
parent permlinkruby
permlinkcreate-a-command-line-gem-from-scratch-with-thor-part-two
titleCreate a command-line gem from scratch with Thor (part two)
Transaction InfoBlock #22991737/Trx 492811ff976988cb2ca6b2f94a3b463e9e7ab833
View Raw JSON Data
{
  "block": 22991737,
  "op": [
    "comment",
    {
      "author": "davidpm",
      "body": "This is the last in a two-part post on creating a Ruby cli gem from scratch. Part one <a href='https://steemit.com/ruby/@davidpm/create-a-command-line-gem-from-scratch-with-thor'>found here</a>.\n\n---\n\nPicking up from our <a href='https://steemit.com/ruby/@davidpm/create-a-command-line-gem-from-scratch-with-thor'>last post</a>, we want to start working on our user interface. In this case we want a cli that looks something like this:\n```\n#-> what would you like to do?\n  1 - Build a matrix\n  2 - Generate a random matrix\n\n#-> now that we have a matrix, would you like to spiralize it?\n  1 - Spiralize it\n  2 - Stare at it\n```\n### Thor\n![](https://ae01.alicdn.com/kf/HTB1FjN5KFXXXXa6XFXXq6xXFXXXx/1-1-Scale-Full-Metal-Thor-Hammer-Mjolnir-1-1-Replica-Thor-Custom-Cosplay-Hammer-Collection.jpg_640x640.jpg)\n\nWe're going to use <a href='https://github.com/erikhuda/thor'>Thor</a>, which is a toolkit for building command-line tools and applications. In fact, `Bundler` itself was written using `Thor`. Let's add it to the bottom of our `gemspec` and re-install:\n\n```\n# spiralizer.gemspec\nspec.add_dependency \"thor\"\n---\n# from cmd line:\n> bundle install\n```\n\nNow, we just need to require it in and start using it!\n\n```\nrequire 'thor'\n...\n```\n\nBut, this is also a good time to start separating our code a bit, so we don't let things get to messy. So, let's create a file called `lib/spiralizer/cli.rb`, and move our `require` statement into this file. Then we will stub out a class under the `Spiralizer` namespace and inherit from `Thor`:\n\n```\nrequire 'thor'\nrequire 'spiralizer'\n\nclass Spiralizer::CLI < Thor\nend\n```\n\nBack in our main module we will need to require in this new file:\n\n```\nrequire \"spiralizer/version\"\nrequire \"spiralizer/cli\"\n\nmodule Spiralizer\n...\nend\n```\n\nOur cli is gem that users can install on their systems. We want them to have an executable that will take a simple command to spin up prompt we defined earlier:\n\nSay the user enters `spiralizer go` for example. We would want output similar to what we envisioned above:\n```\n$ >spiralizer go\n\n#-> what would you like to do?\n  1 - Build a matrix\n  2 - Generate a random matrix\n```\n\nThor gives us some easy-to-use tooling to get this thing going in no time. We'll get started by defining our `go` method, and use the `desc` macro annotation get some magic documentation:\n```\nrequire 'thor'\nrequire 'spiralizer'\n\nclass Spiralizer::CLI < Thor\n  desc 'go', 'starts a prompt that brings users to spiral heaven'\n  def go\n    say \"What would you like to do? (choose a number)\\n 1 - Build a matrix\\n 2 - Generate a random matrix\"\n    action = ask '> '\n    say \"\\nyou picked #{action}\"\n    exit!\n  end\nend\n```\n`Thor` provides some intuitive methods like `say` to output something to the user, and `ask` to prompt the user for input. They work just as expected. Notice that you can assign the result of `ask` to a variable.\n\nTo start playing around with it, we just need to create a new directory called `exe` (alternatively, we could have scaffolded our gem with a `--exe` option but here we are). It should be a sibling directory of `lib` and `bin`. `cd exe && touch spiralizer` to create a file. Then, give it executable permissions: `> chmod +x exe/spiralizer`, and dump this inside:\n```\n#!/usr/bin/env ruby\n\nrequire 'spiralizer/cli'\nSpiralizer::CLI.start\n```\n\n And let's give 'er a whirl:\n\n```\n> bundle exec exe/spiralizer\n\nCommands:\n  spiralizer go              # starts a prompt that brings users to spiral heaven\n  spiralizer help [COMMAND]  # Describe available commands or one specific command\n```\n\nNotice the helpful output Thor gives us. We have two commands available: `go` and `help`. Let's try them out:\n\n```\n> bundle exec exe/spiralizer help\n\nCommands:\n  spiralizer go              # starts a prompt that brings users to spiral heaven\n  spiralizer help [COMMAND]  # Describe available commands or one specific command\n\n> bundle exec exe/spiralizer go\n\nWhat would you like to do? (choose a number)\n 1 - Build a matrix\n 2 - Generate a random matrix\n>  1\n\nyou picked 1\n```\nSo, `help` is the default action, and we can see our new `go` command is registered properly. Right now our prompt gives us two options, let's add functionality for our second option. When a user enters `'2'` we want to spit out a random matrix. We can do a quick and dirty version of this and just hard code some matrix range/dimension pairs that we will feed to our matrix factory. Let's make a constant called `MATRICES` that itself is a matrix:\n\n```\nMATRICES = [\n  ['A-L', '4x3'],\n  ['M-X', '3x4'],\n  ['1-8', '2x4'],\n  ['1-144', '12x12'],\n  ['C-J', '4x2'],\n  ['A-Z', '13x2']\n].freeze\n```\n\nWe now need a semi-random way to pick one of these: `range_response, dimensions = MATRICES[rand(6)]`. This will give us a range value as a string like 'C-j' and dimensions like `4x2`. We'll then split that range string up, so we can feed it into our factory:\n```\nrange = range_response.split('-')\nmatrix = Spiralizer::Matrix.the_matrix(range: (range.first..range.last), dimensions: dimensions)\n```\n\n`Thor` wants you to put helper methods for your class in a `no_commands` block, so let's create one of those\nand put all this together so its somewhat presentable. Here's what we have so far:\n\n```\nclass Spiralizer::CLI < Thor\n  attr_reader :matrix\n\n  MATRICES = [\n    ['A-L', '4x3'],\n    ['M-X', '3x4'],\n    ['1-8', '2x4'],\n    ['1-144', '12x12'],\n    ['C-J', '4x2'],\n    ['A-Z', '13x2']\n  ].freeze\n\n  desc 'go', 'starts a prompt that brings users to spiral heaven'\n  def go\n    say \"What would you like to do? (choose a number)\\n 1 - Build a matrix\\n 2 - Generate a random matrix\"\n    action = ask '> '\n    say \"\\n\"\n\n    range, dimensions = random_range_and_dimensions\n    @matrix = Spiralizer::Matrix.the_matrix(range: range, dimensions: dimensions)\n    output_matrix\n  end\n\n  no_commands do\n    def random_range_and_dimensions\n      say \"\\ngenerating...\"\n      sleep 1\n      range_response, dimensions = MATRICES[rand(6)]\n      range = range_response.split('-')\n      return (range.first..range.last), dimensions\n    end\n\n    def output_matrix\n      say \"\\nHere is your M A T R I X\\n------------------------\"\n      matrix.each { |inner| say inner.join(\"\\t\") }\n      exit!\n    end\n  end\nend\n```\n\nYou'll notice that any input we give the prompt at this point will just generate a random matrix from our list. We're making progress!\n\n```\n> bundle exec exe/spiralizer go\n\nWhat would you like to do? (choose a number)\n 1 - Build a matrix\n 2 - Generate a random matrix\n>  2\n\n\ngenerating...\n\nHere is your M A T R I X\n------------------------\n1 2\n3 4\n5 6\n7 8\n```\n\nNext, let's hook up a follow up prompt that asks us if we want to spiralize the matrix. Update your `output_matrix` method:\n\n```\ndef output_matrix\n  say \"\\nHere is your M A T R I X\\n------------------------\"\n  matrix.each { |arr| say arr.join(\"\\t\") }\n\n  say \"\\nWould you like to spiralize it? (choose a number)\\n 1 - Yes\\n 2 - No\"\n  action = ask '> '\n  say \"\\n\"\n  if action == '1'\n    say Spiralizer::Spiralize.new(matrix: matrix).perform\n  end\n\n  exit!\nend\n```\n\nWe are now prompted for more input after the matrix has been generated! Pretty neat.\n\n```\n> bundle exec exe/spiralizer go\n\nWhat would you like to do? (choose a number)\n 1 - Build a matrix\n 2 - Generate a random matrix\n>  2\n\n\ngenerating...\n\nHere is your M A T R I X\n------------------------\nM N O\nP Q R\nS T U\nV W X\n\nWould you like to spiralize it? (choose a number)\n 1 - Yes\n 2 - No\n>  1\n\nm n o r u x w v s p q t\n```\n\nThis is looking good but we still need to handle our first option to allow users to create their own matrix. Let's add a method to handle that input now:\n\n```\n  def user_range_and_dimensions\n    range_response = ask(\"Please provide range. Acceptable format: (A-L) or (1-6)\\n> \")\n    values         = range_response.split('-')\n    dimensions     = ask(\"Please provide dimensions. Acceptable format: (4x3) or (3x2)\\n> \")\n    return (values.first..values.last), dimensions\n  end\n```\n\nWe want this to trigger when the user chooses `'1'` from our introduction prompt so we need to re-work our `go` method:\n\n```\ndef go\n  say \"What would you like to do? (choose a number)\\n 1 - Build a matrix\\n 2 - Generate a random matrix\"\n  action = ask '> '\n  say \"\\n\"\n\n  case action\n  when '1' then range, dimensions = user_range_and_dimensions\n  when '2' then range, dimensions = random_range_and_dimensions\n  else exit!\n  end\n\n  range, dimensions = random_range_and_dimensions\n  @matrix = Spiralizer::Matrix.the_matrix(range: range, dimensions: dimensions)\n  output_matrix\nrescue Spiralizer::InvalidInput => e\n  say \"\\nUh oh! #{e.message}\"\nensure\n  say \"\\nexiting...\"\n  exit!\nend\n```\n\nYou should be getting the hang of how to use `Thor`. Our class is need of some cleanup, but with some refactoring we end up with something like this:\n\n\n```\nrequire 'thor'\nrequire 'spiralizer'\n\nclass Spiralizer::CLI < Thor\n  MATRICES = [\n    ['A-L', '4x3'],\n    ['M-X', '3x4'],\n    ['1-8', '2x4'],\n    ['1-144', '12x12'],\n    ['C-J', '4x2'],\n    ['A-Z', '13x2']\n  ].freeze\n\n  attr_reader :action, :matrix\n\n  desc 'go', 'starts a prompt that brings users to spiral heaven'\n  def go\n    clear_screen!\n    intro_prompt\n    build_matrix\n    output_matrix\n    action_prompt\n    spiralize!\n  rescue Spiralizer::InvalidInput => e\n    say \"\\nUh oh! #{e.message}\"\n  ensure\n    quit_softly\n  end\n\n  no_commands do\n    def intro_prompt\n      say \"What would you like to do? (choose a number)\\n 1 - Build a matrix\\n 2 - Generate a random matrix\"\n      @action = ask '> '\n    end\n\n    def build_matrix\n      say \"\\n\"\n\n      case action\n      when '1' then range, dimensions = user_range_and_dimensions\n      when '2' then range, dimensions = random_range_and_dimensions\n      else quit_softly\n      end\n\n      @matrix = Spiralizer::Matrix.the_matrix(range: range, dimensions: dimensions)\n    end\n\n    def user_range_and_dimensions\n      range_response = ask(\"Please provide range. Acceptable format: (A-L) or (1-6)\\n> \")\n      values         = range_response.split('-')\n      dimensions     = ask(\"Please provide dimensions. Acceptable format: (4x3) or (3x2)\\n> \")\n      return (values.first..values.last), dimensions\n    end\n\n    def random_range_and_dimensions\n      say \"\\ngenerating...\"\n      pause_for_effect\n      range_response, dimensions = MATRICES[rand(6)]\n      range = range_response.split('-')\n      return (range.first..range.last), dimensions\n    end\n\n    def action_prompt\n      say \"\\nWhat would you like to do with it? (choose a number)\\n 1 - Spiralize it\\n 2 - Look at it\"\n      @action = ask '> '\n    end\n\n    def output_matrix\n      say \"\\nHere is your M A T R I X\\n------------------------\"\n      matrix.each { |inner| say inner.join(\"\\t\") }\n    end\n\n    def spiralize!\n      say \"\\n\"\n      say Spiralizer::Spiralize.new(matrix: matrix).perform if action == '1'\n    end\n\n    def clear_screen!\n      return system 'cls' if Gem.win_platform?\n      system 'clear'\n    end\n\n    def pause_for_effect\n      sleep 0.5\n    end\n\n    def quit_softly\n      say \"\\nexiting...\"\n      pause_for_effect\n      exit!\n    end\n  end\nend\n```\n\nNow, let's try it all out:\n\n```\nWhat would you like to do? (choose a number)\n 1 - Build a matrix\n 2 - Generate a random matrix\n>  1\n\nPlease provide range. Acceptable format: (A-L) or (1-6)\n>  1-6\nPlease provide dimensions. Acceptable format: (4x3) or (3x2)\n>  2x3\n\nHere is your M A T R I X\n------------------------\n1 2\n3 4\n5 6\n\nWhat would you like to do with it? (choose a number)\n 1 - Spiralize it\n 2 - Look at it\n>  1\n\n1 2 4 6 5 3\n\nexiting...\n```\n\nOur little gem is complete! There is a lot of cleanup we can and should do, but this is where this blog post ends.\nFeel free to add new functionality and experiment further with Thor. I've gone ahead and added a `Crisscross` class for\nexample in my <a href=\"https://github.com/david-pm/matrix-spiralizer\">repo</a>.\n\nOh! One more thing, if yoy run `bundle exec rake install` bundler will install this gem on your system as a global executable so you can then call it like this:\n```\n> spiralizer go\n```\n\nIf you ever need to uninstall it for some insane reason just run `gem uninstall spiralizer`.\n\nYou can find a finalized version of the code <a href=\"https://github.com/david-pm/matrix-spiralizer\">here</a>.\n\nWell, there you have it. We've built a cli gem from scratch using `Bundler` and `Thor`. I hope this post serves you well and until next time!\n\n\n### (づ。◕‿‿◕。)づ  *:・゚✧ ✧゚・*:・゚✧ ✧゚・",
      "json_metadata": "{\"tags\":[\"ruby\",\"thor\",\"gem\",\"cli\",\"programming\"],\"image\":[\"https://ae01.alicdn.com/kf/HTB1FjN5KFXXXXa6XFXXq6xXFXXXx/1-1-Scale-Full-Metal-Thor-Hammer-Mjolnir-1-1-Replica-Thor-Custom-Cosplay-Hammer-Collection.jpg_640x640.jpg\"],\"links\":[\"https://steemit.com/ruby/@davidpm/create-a-command-line-gem-from-scratch-with-thor\",\"https://github.com/erikhuda/thor\",\"https://github.com/david-pm/matrix-spiralizer\"],\"app\":\"steemit/0.1\",\"format\":\"markdown\"}",
      "parent_author": "",
      "parent_permlink": "ruby",
      "permlink": "create-a-command-line-gem-from-scratch-with-thor-part-two",
      "title": "Create a command-line gem from scratch with Thor (part two)"
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2018-06-03T06:55:54",
  "trx_id": "492811ff976988cb2ca6b2f94a3b463e9e7ab833",
  "trx_in_block": 40,
  "virtual_op": 0
}
2018/06/03 06:46:45
authordavidpm
permlinkcreate-a-command-line-gem-from-scratch-with-thor
voterdick.sledge
weight85 (0.85%)
Transaction InfoBlock #22991554/Trx bf4fc9bd2eea84b9e35eb7f78b93d02f6f61fe1d
View Raw JSON Data
{
  "block": 22991554,
  "op": [
    "vote",
    {
      "author": "davidpm",
      "permlink": "create-a-command-line-gem-from-scratch-with-thor",
      "voter": "dick.sledge",
      "weight": 85
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2018-06-03T06:46:45",
  "trx_id": "bf4fc9bd2eea84b9e35eb7f78b93d02f6f61fe1d",
  "trx_in_block": 33,
  "virtual_op": 0
}
2018/06/03 06:41:27
authortomask-de
bodyNice read. I leave an upvote for this article *thumbsup*
json metadata{}
parent authordavidpm
parent permlinkcreate-a-command-line-gem-from-scratch-with-thor
permlinkre-davidpm-create-a-command-line-gem-from-scratch-with-thor-20180603t064129266z
titlefossbot voter comment
Transaction InfoBlock #22991448/Trx dac608219c6e88363c4747d0bc6fb8591042ee41
View Raw JSON Data
{
  "block": 22991448,
  "op": [
    "comment",
    {
      "author": "tomask-de",
      "body": "Nice read. I leave an upvote for this article *thumbsup*",
      "json_metadata": "{}",
      "parent_author": "davidpm",
      "parent_permlink": "create-a-command-line-gem-from-scratch-with-thor",
      "permlink": "re-davidpm-create-a-command-line-gem-from-scratch-with-thor-20180603t064129266z",
      "title": "fossbot voter comment"
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2018-06-03T06:41:27",
  "trx_id": "dac608219c6e88363c4747d0bc6fb8591042ee41",
  "trx_in_block": 49,
  "virtual_op": 0
}
2018/06/03 06:41:21
authordavidpm
permlinkcreate-a-command-line-gem-from-scratch-with-thor
votertomask-de
weight10000 (100.00%)
Transaction InfoBlock #22991446/Trx cad4647180f532ee2bc40789ba1c048b9d6be5d8
View Raw JSON Data
{
  "block": 22991446,
  "op": [
    "vote",
    {
      "author": "davidpm",
      "permlink": "create-a-command-line-gem-from-scratch-with-thor",
      "voter": "tomask-de",
      "weight": 10000
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2018-06-03T06:41:21",
  "trx_id": "cad4647180f532ee2bc40789ba1c048b9d6be5d8",
  "trx_in_block": 15,
  "virtual_op": 0
}
2018/06/03 06:28:54
authordavidpm
body@@ -16161,9 +16161,29 @@ reading -. +!%0A%0A### (%E0%B8%87 %CD%A0%C2%B0 %CD%9F%D9%84%CD%9C %CD%A1%C2%B0)%E0%B8%87
json metadata{"tags":["ruby","gem","thor","cli","matrix"],"image":["https://media.giphy.com/media/S4HKH9KgRGMdq/giphy.gif","https://upload.wikimedia.org/wikipedia/commons/3/32/Chrysopoea_of_Cleopatra_1.png"],"links":["https://bundler.io/","https://git-scm.com/","http://rspec.info/","https://github.com/ruby/rake","https://github.com/erikhuda/thor","https://github.com/bundler/bundler/blob/master/lib/bundler/dsl.rb#L58","https://en.wikipedia.org/wiki/Ouroboros"],"app":"steemit/0.1","format":"markdown"}
parent author
parent permlinkruby
permlinkcreate-a-command-line-gem-from-scratch-with-thor
titleCreate a command-line gem from scratch with Thor
Transaction InfoBlock #22991197/Trx 5c7678b75d9ca6c4ede51d4e9258fce84a8ab24c
View Raw JSON Data
{
  "block": 22991197,
  "op": [
    "comment",
    {
      "author": "davidpm",
      "body": "@@ -16161,9 +16161,29 @@\n  reading\n-.\n+!%0A%0A### (%E0%B8%87 %CD%A0%C2%B0 %CD%9F%D9%84%CD%9C %CD%A1%C2%B0)%E0%B8%87\n",
      "json_metadata": "{\"tags\":[\"ruby\",\"gem\",\"thor\",\"cli\",\"matrix\"],\"image\":[\"https://media.giphy.com/media/S4HKH9KgRGMdq/giphy.gif\",\"https://upload.wikimedia.org/wikipedia/commons/3/32/Chrysopoea_of_Cleopatra_1.png\"],\"links\":[\"https://bundler.io/\",\"https://git-scm.com/\",\"http://rspec.info/\",\"https://github.com/ruby/rake\",\"https://github.com/erikhuda/thor\",\"https://github.com/bundler/bundler/blob/master/lib/bundler/dsl.rb#L58\",\"https://en.wikipedia.org/wiki/Ouroboros\"],\"app\":\"steemit/0.1\",\"format\":\"markdown\"}",
      "parent_author": "",
      "parent_permlink": "ruby",
      "permlink": "create-a-command-line-gem-from-scratch-with-thor",
      "title": "Create a command-line gem from scratch with Thor"
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2018-06-03T06:28:54",
  "trx_id": "5c7678b75d9ca6c4ede51d4e9258fce84a8ab24c",
  "trx_in_block": 33,
  "virtual_op": 0
}
2018/06/03 06:17:51
authordavidpm
bodyThis is the first in a two-part post on creating a Ruby command-line gem from scratch. --- Let's write a command-line interface (cli) as a Ruby gem. We need the gem to do something slightly challenging so let's take a popular interview question: ### The Matrix Spiralizer ![](https://media.giphy.com/media/S4HKH9KgRGMdq/giphy.gif) Create an application that takes a matrix (two-dimensional array) and returns a string. The matrix may be of arbitrary size and must consist of uppercase English letters. The returned string must consist of all elements of the matrix ordered in a clockwise spiral pattern starting with element [0, 0]. Each letter must be converted to lowercase and separated by a single whitespace character. For example, given the following matrix: ``` [ [A B C D], [E F G H], [I J K L] ] A B C D E F G H I J K L The resulting string would be: "a b c d h l k j i e f g" ``` Sound fun? Let's jump in. ### Getting Started There are many algorithms you can use to solve this little puzzle, but before we get to the implementation that I went with we need to scaffold our gem. Make sure you have <a href='https://bundler.io/'>Bundler</a> installed (`gem install bundler`) and then: ``` > bundle gem spiralizer && cd spiralizer ``` This command creates a scaffold directory for our new gem and, if we have <a href='https://git-scm.com/'>Git</a> installed, initializes a Git repository in this directory. If this is the first time running the <b>bundle gem</b> command, you will be asked about what kind of default files you'd like to include in all future gems. Let's use <a href='http://rspec.info/'>RSpec</a> for testing our gem. Notice that Bundler scaffolded out a `spec/` directory for our convenience. We need to make sure we specify a recent version of RSpec as a development dependency. Open the <b>spiralizer.gemspec</b> file and bump up the version at the bottom, if needed. While I'm at it, I'm just gonna bump up the <a href='https://github.com/ruby/rake'>Rake</a> and Bundler versions, as well as update the stubbed out summary and description. ``` > spec.summary = %q{A gem that takes a matrix and returns a formatted string} > spec.description = spec.summary ... > spec.add_development_dependency "bundler", "~> 1.16" > spec.add_development_dependency "rake", "~> 11.2" > spec.add_development_dependency "rspec", "~> 3.6" > spec.add_development_dependency "pry" ``` Feel free to bump these versions and add to that description. Later on we will be installing the <a href='https://github.com/erikhuda/thor'>Thor</a> gem, so you could all install that now if you feel like it. In any case, we'll circle back to that later. You may have noticed that we have a `Gemfile`. Open it up and its pretty empty. All you should see is: ``` source "https://rubygems.org" gemspec ``` That `gemspec` method is defined in the Bundler gem under <a href='https://github.com/bundler/bundler/blob/master/lib/bundler/dsl.rb#L58'>lib/bundler/dsl.rb</a>. Because we call that `gemspec` method in our Gemfile, Bundler will automatically add this gem to a group called "development" which we can then reference any time we want to load the gems defined in our gemspec file. Additionally, anybody who now runs `gem install spiralizer --dev` will get these dev dependencies installed too. Run `bundle install` and the gems will be installed, and generate our `Gemfile.lock` (which is responsible for ensuring that every system this library is developed on has the exact same gems). Bundler detects the spiralizer gem, loads the gemspec and bundles just like any other gem. With all this boilerplate out of the way, we can now start addressing our algorithm. ![](https://upload.wikimedia.org/wikipedia/commons/3/32/Chrysopoea_of_Cleopatra_1.png) So, how do we solve our little problem? The solution I came up with works like the <a href="https://en.wikipedia.org/wiki/Ouroboros">Ouroboros</a> above. Say we have with this: ``` [ [A B C D], [E F G H], [I J K L] ] ``` We start at the first index which is this inner array: `[A B C D]`. All we need to do is join that whole row together and we have the beginnings of our desired string. Next, we just need to grab the next index of our outer array, reverse it and join that together. Then we concatenate the result to our previous string, and so on. The approach I'd like to take here is to run this whole thing in a loop that will deconstruct, consume, and discard as we go. When there is nothing left, we know we are done. Hence the reference to the Ouroboros. Here's a semi-visual breakdown of how I want the algorithm to make its way through our matrix: #### Step One: go from left to right until you hit the end, then discard ``` a b c d E F G H I J K L ``` #### Step Two: grab last index of remaining inner arrays, then discard ``` E F G h I J K l ``` #### Step Three: take the last array, reverse crawl it, then discard ``` E F G i j k ``` #### Step Four: grab first index of remaining inner arrays, climb the ladder, then discard ``` e F G ``` Then repeat until there is nothing left to consume. Let's write a simple test to get this going. Open up `spec/spiralizer_spec.rb` and you should see some examples scaffolded for you. The first one is useful, so we'll keep it around: ``` it "has a version number" do expect(Spiralizer::VERSION).not_to be nil end ``` If you run the spec (`rspec spec/spiralizer_spec.rb:2`) it will pass. That is because bundler defined a `VERSION` constant for us already in `lib/spiralizer/version.rb`. So, that's nice. Let's delete that second example that "does something useful", and get back to our implementation. We want to define a useful example that will help us work towards our algorithm we defined above. Let's start by defining a matrix of chars and an expected result string: ``` RSpec.describe Spiralizer do context 'character matrix' do let(:matrix) do [%w[A B C D], %w[E F G H], %w[I J K L], %w[M N O P], %w[Q R S T]] end let(:expected_result) { %Q{a b c d h l p t s r q m i e f g k o n j} } it 'returns a string in spiraling order' do end end end ``` Now, we just need an assertion. We want to pass our matrix in, and get back our expected string. So, what do we want our interface to look like? In `lib/spiralizer.rb` you'll notice a comment from bundler saying: `# Your code goes here...` We will get to work in there. Let's add a nested class called `class Spiralizer` that will have a `perform` method that does the work. Here's our assertion: ``` RSpec.describe Spiralizer do context 'character matrix' do let(:matrix) do [%w[A B C D], %w[E F G H], %w[I J K L], %w[M N O P], %w[Q R S T]] end let(:expected_result) { %Q{a b c d h l p t s r q m i e f g k o n j} } it 'returns a string in spiraling order' do expect(Spiralizer::Spiralize.new(matrix: matrix).perform).to eq expected_result end end end ``` Let's add one more example for handling a matrix of integers: ``` RSpec.describe Spiralizer do ... context 'integer matrix' do let(:matrix) do [[1,2,3], [10, 11, 4], [9, 12, 5], [8, 7, 6]] end let(:expected_result) { %Q{1 2 3 4 5 6 7 8 9 10 11 12} } it 'handles numbers with multiple digits' do expect(Spiralizer::Spiralize.new(matrix: matrix).perform).to eq expected_result end end end ``` It's pretty much the same thing. We want to pass in our pre-defined matrix (note the keyword argument) and spiralize it. Right now this test is red. If you want to try your own solution, now would be the time to pause reading further and give it a try. --- Skipping ahead for brevity, here is a solution I came up with: ``` class Spiralizer::Spiralize def initialize(matrix:) @matrix = matrix @result = String.new @padding = String.new ' ' end def perform until matrix.empty? do @result += @matrix.shift.join(padding).concat(padding) break if matrix.empty? @result += @matrix.map(&:pop).join(padding).concat(padding) @result += @matrix.pop.reverse.join(padding).concat(padding) @result += @matrix.map(&:shift).reverse.join(padding).concat(padding) end @result.downcase.strip end private attr_reader :padding, :result, :matrix end ``` Our `Spiralizer::Spiralize` class creates a few instance variables for our pre-defined matrix, and a couple of strings: one empty, and one with a space for adding padding to our string output. `perform` does the work by looping over each inner array of characters, shifting and popping as it goes until the matrix is consumed. This took a little trial and error to figure out, but I think the result is simple enough to follow. Our specs should now be passing. So, this is working just fine but what happens if we enter some bad input? Let's add some naive validations to at least make sure we are only accept valid matrices. I'm imagining we want our validations to run immediately when we receive input from a user. We can define a simple error class called `Spiralizer::InvalidInput`, and a class method on our spiralizer module that take our matrix in as an argument and analyzes its <em>matrixiness</em>. If it is not a matrix it will raise our error class. Pretty straight forward. Here's my test: ``` RSpec.describe Spiralizer do ... context 'invalid input' do let(:notamatrix) { [{a: 'b'}, 'bye'] } it 'only takes matrices' do expect{ Spiralizer::Spiralize.new(matrix: notamatrix) } .to raise_error Spiralizer::InvalidInput end end end ``` Our tests are red again, so let's add our code: ``` module Spiralizer class InvalidInput < StandardError; end def self.validate_input(matrix) unless matrix.respond_to?(:first) && matrix.first.respond_to?(:join) fail Spiralizer::InvalidInput.new("spiralize only accepts a matrix") end end class Spiralize def initialize(matrix:) Spiralizer.validate_input(matrix) ... end ... end end ``` ...and run our tests again. Green. This is a good time to think a little about what kind of application we'd like. Let's go for a command line interface that prompts a user for input to spiralize. When we start up the program we'd like a list of options to choose from, so the user can know what they are able to do. For example: ``` #-> what would you like to do? 1 - Build a matrix 2 - Generate a random matrix #-> now that we have a matrix, would you like to spiralize it? 1 - spiralize it, duh 2 - nope ``` In order to get this going were going to have to add a few missing pieces to our baby gem. We're going to want to add a matrix factory that can handle user input. Without matrices to spiralize this gem is no good. We'll also need to build out our user facing cli. Let's add some specs for a matrix factory. Our factory will be class within the same namespace called Matrix. We would like to pass in some values and paramaters on how we'd like this matrix to look. Let's say a range of characters and the dimensions of the matrix. For example, if we had a range of characters `('A'..'L')` we could build a `3x4`, `2x6`, or a `4x3` matrix: ``` (2 x 6) A B C D E F G H I J K L (3x4) A B C D E F G H I J K L (4x3) A B C D E F G H I J K L ``` So, we need to be able to accept a range of values, and the desired dimensions of our matrix. Let's mix it up and make it a class method called `the_matrix` just 'cause. We'll add our first assertion right under the others for now: ``` RSpec.describe Spiralizer do ... describe 'Matrix' do context 'character matrix' do let(:matrix) do Spiralizer::Matrix.the_matrix(range: 'A'..'L', dimensions: '4x3') end let(:expected_matrix) do [%w[A B C D], %w[E F G H], %w[I J K L]] end it 'returns a matrix from a given range of letters' do expect(matrix).to eq expected_matrix end end end ``` The happy path is easy enough but we are talking about user input so we need to add tests for as many cases we can conceive of and get to work. I'm not trying to be exhaustive here, since this is just for fun. Here is where I stopped: ``` RSpec.describe Spiralizer do ... describe 'Matrix' do ... context 'uneven range of numbers' do let(:matrix) do Spiralizer::Matrix.the_matrix(range: 1..7, dimensions: '3x2') end let(:expected_matrix) do [[1,2,3], [4,5,6]] end it 'returns a matrix from a given range of numbers, excluding last number' do expect(matrix).to eq expected_matrix end end context 'invalid input' do it 'raises when range not passed' do expect{ Spiralizer::Matrix.the_matrix(range: 'hi', dimensions: '3x2') } .to raise_error Spiralizer::InvalidInput, 'valid range expected' end it 'raises when mal-formatted dimensions are passed' do expect{ Spiralizer::Matrix.the_matrix(range: (1..4), dimensions: '3 2') } .to raise_error Spiralizer::InvalidInput, 'dimensions must be a string in \'2x4\' format' end it 'raises when matrix cannot be built with given input' do expect{ Spiralizer::Matrix.the_matrix(range: (1..4), dimensions: '3x2') } .to raise_error Spiralizer::InvalidInput, 'can\'t build matrix with range/dimensions pair' end end end end ``` As you can see, I'm anticipating a user could pass in an uneven total of characters. We don't want to deal with odd numbers, so we'll just lop that last char off and move on. We also guard against malformatted ranges, and dimensions, and just give up when we can't build a matrix with the input supplied. And our tests are now red again. Let's get those passing: ``` class Matrix attr_reader :range, :dimensions INVALID_PAIR = 'can\'t build matrix with range/dimensions pair'.freeze INVALID_RANGE = 'valid range expected'.freeze INVALID_DIMENSIONS = 'dimensions must be a string in \'2x4\' format'.freeze def self.the_matrix(range:, dimensions:) new(range, dimensions).build end def initialize(range, dimensions) @range = range @dimensions = dimensions validate_range_and_dimenstions end def validate_range_and_dimenstions fail Spiralizer::InvalidInput.new(INVALID_RANGE) unless valid_range? fail Spiralizer::InvalidInput.new(INVALID_DIMENSIONS) unless valid_dimensions? fail Spiralizer::InvalidInput.new(INVALID_PAIR) unless valid_pair? puts('WARNING: The last character will be excluded.') if range.count.odd? end def build range_enum = range.each Array.new(cols) { Array.new(rows) { range_enum.next } } end private def valid_range? range.respond_to?(:to_a) end def valid_dimensions? dimensions.respond_to?(:split) && dimensions =~ /\d+\s?x+\s?\d+/ end def valid_pair? (range.count / rows) == cols || (range.count / cols) == rows end def split_dimensions @split_dimensions ||= dimensions.split('x').map(&:to_i) end def rows @rows ||= split_dimensions.first end def cols @cols ||= split_dimensions.last end end ``` As mentioned above, our entry point will be from `the_matrix` class method. Feel free not to do that, I just wanted to have a method called `the_matrix`. :) We new up the object from there passing in our params and validate the input with the `validate_range_and_dimenstions` method. If the input passes the validations we return back to `the_matrix` and call `build` on our newly instantiated object. `build` created an enum out of our range and calls Enumerator's `next` method to fill in the values of our matrix nested inside of the inner columns of our initial array. I've glossed over some of the complexities involved, but going into Ruby's enum classes is outside of the scope of this post. Take a few minutes to look over the code as needed. At this point, our tests are green again. We can now create matrices and spiralize them, and we have decent test coverage to boot. --- In part two of this series, we will begin working on our cli making use of the Thor gem. Until then, thanks for reading.
json metadata{"tags":["ruby","gem","thor","cli","matrix"],"image":["https://media.giphy.com/media/S4HKH9KgRGMdq/giphy.gif","https://upload.wikimedia.org/wikipedia/commons/3/32/Chrysopoea_of_Cleopatra_1.png"],"links":["https://bundler.io/","https://git-scm.com/","http://rspec.info/","https://github.com/ruby/rake","https://github.com/erikhuda/thor","https://github.com/bundler/bundler/blob/master/lib/bundler/dsl.rb#L58","https://en.wikipedia.org/wiki/Ouroboros"],"app":"steemit/0.1","format":"markdown"}
parent author
parent permlinkruby
permlinkcreate-a-command-line-gem-from-scratch-with-thor
titleCreate a command-line gem from scratch with Thor
Transaction InfoBlock #22990976/Trx 5093a62f53ca9d1ef8dc353e574df643adf2c7a1
View Raw JSON Data
{
  "block": 22990976,
  "op": [
    "comment",
    {
      "author": "davidpm",
      "body": "This is the first in a two-part post on creating a Ruby command-line gem from scratch.\n\n---\n\nLet's write a command-line interface (cli) as a Ruby gem. We need the gem to do something slightly challenging so let's take a popular interview question: \n\n### The Matrix Spiralizer\n\n![](https://media.giphy.com/media/S4HKH9KgRGMdq/giphy.gif)\n\nCreate an application that takes a matrix (two-dimensional array) and returns a string. The matrix may be of arbitrary size and must consist of uppercase English letters. The returned string must consist of all elements of the matrix ordered in a clockwise spiral pattern starting with element [0, 0]. Each letter must be converted to lowercase and separated by a single whitespace character.\n\nFor example, given the following matrix:\n```\n[\n  [A B C D],\n  [E F G H],\n  [I J K L]\n]\n\nA B C D\nE F G H\nI J K L\n\nThe resulting string would be:\n\"a b c d h l k j i e f g\"\n```\n\nSound fun? Let's jump in.\n\n\n### Getting Started\n\nThere are many algorithms you can use to solve this little puzzle, but before we get to the implementation that I went with we need to scaffold our gem. Make sure you have <a href='https://bundler.io/'>Bundler</a>  installed (`gem install bundler`) and then:\n\n```\n> bundle gem spiralizer && cd spiralizer\n```\n\nThis command creates a scaffold directory for our new gem and, if we have <a href='https://git-scm.com/'>Git</a> installed, initializes a Git repository in this directory. If this is the first time running the <b>bundle gem</b> command, you will be asked about what kind of default files you'd like to include in all future gems. Let's use <a href='http://rspec.info/'>RSpec</a> for testing our gem. Notice that Bundler scaffolded out a `spec/` directory for our convenience. We need to make sure we specify a recent version of RSpec as a development dependency.\n\nOpen the <b>spiralizer.gemspec</b> file and bump up the version at the bottom, if needed. While I'm at it, I'm just gonna bump up the <a href='https://github.com/ruby/rake'>Rake</a> and Bundler versions, as well as update the stubbed out summary and description.\n\n```\n> spec.summary     = %q{A gem that takes a matrix and returns a formatted string}\n> spec.description = spec.summary\n...\n> spec.add_development_dependency \"bundler\", \"~> 1.16\"\n> spec.add_development_dependency \"rake\", \"~> 11.2\"\n> spec.add_development_dependency \"rspec\", \"~> 3.6\"\n> spec.add_development_dependency \"pry\"\n```\n\nFeel free to bump these versions and add to that description. Later on we will be installing the <a href='https://github.com/erikhuda/thor'>Thor</a> gem, so you could all install that now if you feel like it. In any case, we'll circle back to that later.\n\nYou may have noticed that we have a `Gemfile`. Open it up and its pretty empty. All you should see is:\n```\nsource \"https://rubygems.org\"\ngemspec\n```\n\nThat `gemspec` method is defined in the Bundler gem under <a href='https://github.com/bundler/bundler/blob/master/lib/bundler/dsl.rb#L58'>lib/bundler/dsl.rb</a>. Because we call that `gemspec` method in our Gemfile, Bundler will automatically add this gem to a group called \"development\" which we can then reference any time we want to load the gems defined in our gemspec file. Additionally, anybody who now runs `gem install spiralizer --dev` will get these dev dependencies installed too.\n\nRun `bundle install` and the gems will be installed, and generate our `Gemfile.lock` (which is responsible for ensuring that every system this library is developed on has the exact same gems). Bundler detects the spiralizer gem, loads the gemspec and bundles just like any other gem. With all this boilerplate out of the way, we can now start addressing our algorithm.\n\n![](https://upload.wikimedia.org/wikipedia/commons/3/32/Chrysopoea_of_Cleopatra_1.png)\n\nSo, how do we solve our little problem? The solution I came up with works like the <a href=\"https://en.wikipedia.org/wiki/Ouroboros\">Ouroboros</a> above. Say we have with this:\n\n```\n[\n  [A B C D],\n  [E F G H],\n  [I J K L]\n]\n```\nWe start at the first index which is this inner array: `[A B C D]`. All we need to do is join that whole row together and we have the beginnings of our desired string. Next, we just need to grab the next index of our outer array, reverse it and join that together. Then we concatenate the result to our previous string, and so on. The approach I'd like to take here is to run this whole thing in a loop that will deconstruct, consume, and discard as we go. When there is nothing left, we know we are done. Hence the reference to the Ouroboros. Here's a semi-visual breakdown of how I want the algorithm to make its way through our matrix:\n\n#### Step One:\ngo from left to right until you hit the end, then discard\n```\n  a b c d\n  E F G H\n  I J K L\n```\n#### Step Two:\ngrab last index of remaining inner arrays, then discard\n```\n  E F G h\n  I J K l\n```\n#### Step Three:\ntake the last array, reverse crawl it, then discard\n```\n  E F G\n  i j k\n```\n#### Step Four:\ngrab first index of remaining inner arrays, climb the ladder, then discard\n```\n  e F G\n```\n\nThen repeat until there is nothing left to consume.\n\nLet's write a simple test to get this going. Open up `spec/spiralizer_spec.rb` and you should see some examples scaffolded for you. The first one is useful, so we'll keep it around:\n\n```\n  it \"has a version number\" do\n    expect(Spiralizer::VERSION).not_to be nil\n  end\n```\n\nIf you run the spec (`rspec spec/spiralizer_spec.rb:2`) it will pass. That is because bundler defined a `VERSION` constant for us already in `lib/spiralizer/version.rb`. So, that's nice. Let's delete that second example that \"does something useful\", and get back to our implementation.\n\nWe want to define a useful example that will help us work towards our algorithm we defined above. Let's start by defining a matrix of chars and an expected result string:\n\n```\nRSpec.describe Spiralizer do\n  context 'character matrix' do\n    let(:matrix) do\n      [%w[A B C D], %w[E F G H], %w[I J K L], %w[M N O P], %w[Q R S T]]\n    end\n\n    let(:expected_result) { %Q{a b c d h l p t s r q m i e f g k o n j} }\n\n    it 'returns a string in spiraling order' do\n    end\n  end\nend\n```\n\nNow, we just need an assertion. We want to pass our matrix in, and get back our expected string. So, what do we want our interface to look like? In `lib/spiralizer.rb` you'll notice a comment from bundler saying: `# Your code goes here...`\n\nWe will get to work in there. Let's add a nested class called `class Spiralizer` that will have a `perform` method that does the work. Here's our assertion:\n\n```\nRSpec.describe Spiralizer do\n  context 'character matrix' do\n    let(:matrix) do\n      [%w[A B C D], %w[E F G H], %w[I J K L], %w[M N O P], %w[Q R S T]]\n    end\n\n    let(:expected_result) { %Q{a b c d h l p t s r q m i e f g k o n j} }\n\n    it 'returns a string in spiraling order' do\n      expect(Spiralizer::Spiralize.new(matrix: matrix).perform).to eq expected_result\n    end\n  end\nend\n```\n\nLet's add one more example for handling a matrix of integers:\n\n```\nRSpec.describe Spiralizer do\n  ...\n  context 'integer matrix' do\n    let(:matrix) do\n      [[1,2,3], [10, 11, 4], [9, 12, 5], [8, 7, 6]]\n    end\n\n    let(:expected_result) { %Q{1 2 3 4 5 6 7 8 9 10 11 12} }\n\n    it 'handles numbers with multiple digits' do\n      expect(Spiralizer::Spiralize.new(matrix: matrix).perform).to eq expected_result\n    end\n  end\nend\n```\n\nIt's pretty much the same thing. We want to pass in our pre-defined matrix (note the keyword argument) and spiralize it. Right now this test is red. If you want to try your own solution, now would be the time to pause reading further and give it a try.\n\n---\n\nSkipping ahead for brevity, here is a solution I came up with:\n\n```\nclass Spiralizer::Spiralize\n  def initialize(matrix:)\n    @matrix  = matrix\n    @result  = String.new\n    @padding = String.new ' '\n  end\n\n  def perform\n    until matrix.empty? do\n      @result += @matrix.shift.join(padding).concat(padding)\n      break if matrix.empty?\n      @result += @matrix.map(&:pop).join(padding).concat(padding)\n      @result += @matrix.pop.reverse.join(padding).concat(padding)\n      @result += @matrix.map(&:shift).reverse.join(padding).concat(padding)\n    end\n\n    @result.downcase.strip\n  end\n\n  private\n\n  attr_reader :padding, :result, :matrix\nend\n```\n\nOur `Spiralizer::Spiralize` class creates a few instance variables for our pre-defined matrix, and a couple of strings: one empty, and one with a space for adding padding to our string output. `perform` does the work by looping over each inner array of characters, shifting and popping as it goes until the matrix is consumed. This took a little trial and error to figure out, but I think the result is simple enough to follow. Our specs should now be passing.\n\nSo, this is working just fine but what happens if we enter some bad input? Let's add some naive validations to at least make sure we are only accept valid matrices. I'm imagining we want our validations to run immediately when we receive input from a user. We can define a simple error class called `Spiralizer::InvalidInput`, and a class method on our\nspiralizer module that take our matrix in as an argument and analyzes its <em>matrixiness</em>. If it is not a matrix it will raise our error class. Pretty straight forward. Here's my test:\n\n```\nRSpec.describe Spiralizer do\n  ...\n  context 'invalid input' do\n    let(:notamatrix) { [{a: 'b'}, 'bye'] }\n\n    it 'only takes matrices' do\n      expect{ Spiralizer::Spiralize.new(matrix: notamatrix) }\n        .to raise_error Spiralizer::InvalidInput\n    end\n  end\nend\n```\n\nOur tests are red again, so let's add our code: \n\n```\nmodule Spiralizer\n  class InvalidInput < StandardError; end\n\n  def self.validate_input(matrix)\n    unless matrix.respond_to?(:first) && matrix.first.respond_to?(:join)\n      fail Spiralizer::InvalidInput.new(\"spiralize only accepts a matrix\")\n    end\n  end\n\n  class Spiralize\n    def initialize(matrix:)\n      Spiralizer.validate_input(matrix)\n      ...\n    end\n    ...\n  end\nend\n```\n\n...and run our tests again. Green.\n\nThis is a good time to think a little about what kind of application we'd like. Let's go for a command line interface that prompts a user for input to spiralize.  When we start up the program we'd like a list of options to choose from, so the user can know what they are able to do. For example:\n\n```\n#-> what would you like to do?\n  1 - Build a matrix\n  2 - Generate a random matrix\n\n#-> now that we have a matrix, would you like to spiralize it?\n  1 - spiralize it, duh\n  2 - nope\n```\n\nIn order to get this going were going to have to add a few missing pieces to our baby gem. We're going to want to add a matrix factory that can handle user input. Without matrices to spiralize this gem is no good. We'll also need to build out our user facing cli. Let's add some specs for a matrix factory. Our factory will be class within the same namespace called Matrix. We would like to pass in some values and paramaters on how we'd like this matrix to look. Let's say a range of characters and the dimensions of the matrix.\n\nFor example, if we had a range of characters `('A'..'L')` we could build a `3x4`, `2x6`, or a `4x3` matrix:\n\n```\n(2 x 6)\n\nA B\nC D\nE F\nG H\nI J\nK L\n\n(3x4)\n\nA B C\nD E F\nG H I\nJ K L\n\n(4x3)\n\nA B C D\nE F G H\nI J K L\n```\n\nSo, we need to be able to accept a range of values, and the desired dimensions of our matrix. Let's mix it up and make it a class method called `the_matrix` just 'cause. We'll add our first assertion right under the others for now:\n```\nRSpec.describe Spiralizer do\n  ...\n  describe 'Matrix' do\n    context 'character matrix' do\n      let(:matrix) do\n        Spiralizer::Matrix.the_matrix(range: 'A'..'L', dimensions: '4x3')\n      end\n\n      let(:expected_matrix) do\n        [%w[A B C D], %w[E F G H], %w[I J K L]]\n      end\n\n      it 'returns a matrix from a given range of letters' do\n        expect(matrix).to eq expected_matrix\n      end\n    end\n  end\n```\nThe happy path is easy enough but we are talking about user input so we need to add tests for as many cases we can conceive of and get to work. I'm not trying to be exhaustive here, since this is just for fun. Here is where I stopped:\n\n```\nRSpec.describe Spiralizer do\n  ...\n  describe 'Matrix' do\n    ...\n    context 'uneven range of numbers' do\n      let(:matrix) do\n        Spiralizer::Matrix.the_matrix(range: 1..7, dimensions: '3x2')\n      end\n\n      let(:expected_matrix) do\n        [[1,2,3], [4,5,6]]\n      end\n\n      it 'returns a matrix from a given range of numbers, excluding last number' do\n        expect(matrix).to eq expected_matrix\n      end\n    end\n\n    context 'invalid input' do\n      it 'raises when range not passed' do\n        expect{ Spiralizer::Matrix.the_matrix(range: 'hi', dimensions: '3x2') }\n          .to raise_error Spiralizer::InvalidInput, 'valid range expected'\n      end\n\n      it 'raises when mal-formatted dimensions are passed' do\n        expect{ Spiralizer::Matrix.the_matrix(range: (1..4), dimensions: '3 2') }\n          .to raise_error Spiralizer::InvalidInput, 'dimensions must be a string in \\'2x4\\' format'\n      end\n\n      it 'raises when matrix cannot be built with given input' do\n        expect{ Spiralizer::Matrix.the_matrix(range: (1..4), dimensions: '3x2') }\n          .to raise_error Spiralizer::InvalidInput, 'can\\'t build matrix with range/dimensions pair'\n      end\n    end\n  end\nend\n```\nAs you can see, I'm anticipating a user could pass in an uneven total of characters. We don't want to deal with odd numbers, so we'll just lop that last char off and move on. We also guard against malformatted ranges, and dimensions, and just give up when we can't build a matrix with the input supplied. And our tests are now red again.\n\nLet's get those passing:\n```\nclass Matrix\n  attr_reader :range, :dimensions\n\n  INVALID_PAIR       = 'can\\'t build matrix with range/dimensions pair'.freeze\n  INVALID_RANGE      = 'valid range expected'.freeze\n  INVALID_DIMENSIONS = 'dimensions must be a string in \\'2x4\\' format'.freeze\n\n  def self.the_matrix(range:, dimensions:)\n    new(range, dimensions).build\n  end\n\n  def initialize(range, dimensions)\n    @range = range\n    @dimensions = dimensions\n    validate_range_and_dimenstions\n  end\n\n  def validate_range_and_dimenstions\n    fail Spiralizer::InvalidInput.new(INVALID_RANGE)      unless valid_range?\n    fail Spiralizer::InvalidInput.new(INVALID_DIMENSIONS) unless valid_dimensions?\n    fail Spiralizer::InvalidInput.new(INVALID_PAIR)       unless valid_pair?\n\n    puts('WARNING: The last character will be excluded.') if range.count.odd?\n  end\n\n  def build\n    range_enum = range.each\n    Array.new(cols) { Array.new(rows) { range_enum.next } }\n  end\n\n  private\n\n  def valid_range?\n    range.respond_to?(:to_a)\n  end\n\n  def valid_dimensions?\n    dimensions.respond_to?(:split) && dimensions =~ /\\d+\\s?x+\\s?\\d+/\n  end\n\n  def valid_pair?\n    (range.count / rows) == cols || (range.count / cols) == rows\n  end\n\n  def split_dimensions\n    @split_dimensions ||= dimensions.split('x').map(&:to_i)\n  end\n\n  def rows\n    @rows ||= split_dimensions.first\n  end\n\n  def cols\n    @cols ||= split_dimensions.last\n  end\nend\n```\n\nAs mentioned above, our entry point will be from `the_matrix` class method. Feel free not to do that, I just wanted to have a method called `the_matrix`. :)\n\nWe new up the object from there passing in our params and validate the input with the `validate_range_and_dimenstions` method. If the input passes the validations we return back to `the_matrix` and call `build` on our newly instantiated object. `build` created an enum out of our range and calls Enumerator's `next` method to fill in the values of our matrix nested inside of the inner columns of our initial array.\n\nI've glossed over some of the complexities involved, but going into Ruby's enum classes is outside of the scope of this post. Take a few minutes to look over the code as needed. At this point, our tests are green again. We can now create matrices and spiralize them, and we have decent test coverage to boot. \n\n---\n\nIn part two of this series, we will begin working on our cli making use of the Thor gem. Until then, thanks for reading.",
      "json_metadata": "{\"tags\":[\"ruby\",\"gem\",\"thor\",\"cli\",\"matrix\"],\"image\":[\"https://media.giphy.com/media/S4HKH9KgRGMdq/giphy.gif\",\"https://upload.wikimedia.org/wikipedia/commons/3/32/Chrysopoea_of_Cleopatra_1.png\"],\"links\":[\"https://bundler.io/\",\"https://git-scm.com/\",\"http://rspec.info/\",\"https://github.com/ruby/rake\",\"https://github.com/erikhuda/thor\",\"https://github.com/bundler/bundler/blob/master/lib/bundler/dsl.rb#L58\",\"https://en.wikipedia.org/wiki/Ouroboros\"],\"app\":\"steemit/0.1\",\"format\":\"markdown\"}",
      "parent_author": "",
      "parent_permlink": "ruby",
      "permlink": "create-a-command-line-gem-from-scratch-with-thor",
      "title": "Create a command-line gem from scratch with Thor"
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2018-06-03T06:17:51",
  "trx_id": "5093a62f53ca9d1ef8dc353e574df643adf2c7a1",
  "trx_in_block": 16,
  "virtual_op": 0
}
2018/05/29 01:57:00
authordavidpm
permlinkblockchain-proof-of-concept
voterlionindayard
weight49 (0.49%)
Transaction InfoBlock #22841809/Trx 61c8d06fef8cc940ff53588112356d48ba253c60
View Raw JSON Data
{
  "block": 22841809,
  "op": [
    "vote",
    {
      "author": "davidpm",
      "permlink": "blockchain-proof-of-concept",
      "voter": "lionindayard",
      "weight": 49
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2018-05-29T01:57:00",
  "trx_id": "61c8d06fef8cc940ff53588112356d48ba253c60",
  "trx_in_block": 20,
  "virtual_op": 0
}
2018/05/29 01:56:57
authordavidpm
permlinkblockchain-proof-of-concept
voterdick.sledge
weight49 (0.49%)
Transaction InfoBlock #22841808/Trx 43bc6124dd46b5b53ef2caca7f0d169b6c0c5bfd
View Raw JSON Data
{
  "block": 22841808,
  "op": [
    "vote",
    {
      "author": "davidpm",
      "permlink": "blockchain-proof-of-concept",
      "voter": "dick.sledge",
      "weight": 49
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2018-05-29T01:56:57",
  "trx_id": "43bc6124dd46b5b53ef2caca7f0d169b6c0c5bfd",
  "trx_in_block": 15,
  "virtual_op": 0
}
2018/05/29 01:34:12
authordavidpm
body@@ -409,12 +409,8 @@ ord%0A -%3Cbr%3E %0A*Bl @@ -3181,16 +3181,281 @@ iated.%0A%0A +%60%60%60%0Aclass Block%0A ...%0A def initialize(index:, data:, parent: '')%0A ...%0A @hash = hashsum%0A end%0A%0A private%0A%0A def hashsum%0A hashable = %22#%7Bindex.to_s%7D #%7Bparent%7D #%7Bcreated_at%7D #%7Bdata.to_s%7D%22%0A Digest::SHA2.new(256).hexdigest(hashable)%0A end%0Aend%0A%60%60%60%0A%0A %60hashsum
json metadata{"tags":["blockchain","ruby","crypto","programming"],"image":["https://themerkle.com/wp-content/uploads/2017/08/shutterstock_679065259.jpg"],"links":["https://www.ruby-lang.org/en/","https://ruby-doc.org/stdlib-2.5.0/libdoc/digest/rdoc/Digest/SHA2.html","https://en.wikipedia.org/wiki/SOLID","https://gist.github.com/david-pm/4791fbb0aa2a0cf4efce9adb044352d3"],"app":"steemit/0.1","format":"markdown"}
parent author
parent permlinkblockchain
permlinkblockchain-proof-of-concept
titleBlockchain Proof of Concept
Transaction InfoBlock #22841353/Trx 42c1a2ddb067eed5c1bf860dda62f0e61671cb92
View Raw JSON Data
{
  "block": 22841353,
  "op": [
    "comment",
    {
      "author": "davidpm",
      "body": "@@ -409,12 +409,8 @@\n ord%0A\n-%3Cbr%3E\n %0A*Bl\n@@ -3181,16 +3181,281 @@\n iated.%0A%0A\n+%60%60%60%0Aclass Block%0A  ...%0A  def initialize(index:, data:, parent: '')%0A     ...%0A    @hash          = hashsum%0A  end%0A%0A  private%0A%0A  def hashsum%0A    hashable = %22#%7Bindex.to_s%7D #%7Bparent%7D #%7Bcreated_at%7D #%7Bdata.to_s%7D%22%0A    Digest::SHA2.new(256).hexdigest(hashable)%0A  end%0Aend%0A%60%60%60%0A%0A\n %60hashsum\n",
      "json_metadata": "{\"tags\":[\"blockchain\",\"ruby\",\"crypto\",\"programming\"],\"image\":[\"https://themerkle.com/wp-content/uploads/2017/08/shutterstock_679065259.jpg\"],\"links\":[\"https://www.ruby-lang.org/en/\",\"https://ruby-doc.org/stdlib-2.5.0/libdoc/digest/rdoc/Digest/SHA2.html\",\"https://en.wikipedia.org/wiki/SOLID\",\"https://gist.github.com/david-pm/4791fbb0aa2a0cf4efce9adb044352d3\"],\"app\":\"steemit/0.1\",\"format\":\"markdown\"}",
      "parent_author": "",
      "parent_permlink": "blockchain",
      "permlink": "blockchain-proof-of-concept",
      "title": "Blockchain Proof of Concept"
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2018-05-29T01:34:12",
  "trx_id": "42c1a2ddb067eed5c1bf860dda62f0e61671cb92",
  "trx_in_block": 46,
  "virtual_op": 0
}

Account Metadata

POSTING JSON METADATA
profile{"profile_image":"https://avatars3.githubusercontent.com/u/6234571?s=400&u=81563e5c8010d6ffdb26be7d77dad7c555c6066f&v=4","website":"http://davidpm.io","cover_image":"https://pbs.twimg.com/profile_banners/948220550/1503333176/1500x500","about":"quick posts about programming"}
JSON METADATA
profile{"profile_image":"https://avatars3.githubusercontent.com/u/6234571?s=400&u=81563e5c8010d6ffdb26be7d77dad7c555c6066f&v=4","website":"http://davidpm.io","cover_image":"https://pbs.twimg.com/profile_banners/948220550/1503333176/1500x500","about":"quick posts about programming"}
{
  "posting_json_metadata": {
    "profile": {
      "profile_image": "https://avatars3.githubusercontent.com/u/6234571?s=400&u=81563e5c8010d6ffdb26be7d77dad7c555c6066f&v=4",
      "website": "http://davidpm.io",
      "cover_image": "https://pbs.twimg.com/profile_banners/948220550/1503333176/1500x500",
      "about": "quick posts about programming"
    }
  },
  "json_metadata": {
    "profile": {
      "profile_image": "https://avatars3.githubusercontent.com/u/6234571?s=400&u=81563e5c8010d6ffdb26be7d77dad7c555c6066f&v=4",
      "website": "http://davidpm.io",
      "cover_image": "https://pbs.twimg.com/profile_banners/948220550/1503333176/1500x500",
      "about": "quick posts about programming"
    }
  }
}

Auth Keys

Owner
Single Signature
Public Keys
STM5DEkfgR4JvnCFgdPyqbyseWJS4ePqHj9VnBbLVadAJuCQErBpy1/1
Active
Single Signature
Public Keys
STM8kxLaTeG6HXYMuVtjr6R1yFcDpob1H4R7HuWj3CpQDKB2rhRnZ1/1
Posting
Single Signature
Public Keys
STM5D1ZCtab5A5DzioThZSGKCASa4QuYXsegaFYr4E2edcKa85ciP1/1
Memo
STM6P3LvVZjSRnWetfbWcXeEcF6smw5xpW87s43oEd21Lea53ovPb
{
  "owner": {
    "account_auths": [],
    "key_auths": [
      [
        "STM5DEkfgR4JvnCFgdPyqbyseWJS4ePqHj9VnBbLVadAJuCQErBpy",
        1
      ]
    ],
    "weight_threshold": 1
  },
  "active": {
    "account_auths": [],
    "key_auths": [
      [
        "STM8kxLaTeG6HXYMuVtjr6R1yFcDpob1H4R7HuWj3CpQDKB2rhRnZ",
        1
      ]
    ],
    "weight_threshold": 1
  },
  "posting": {
    "account_auths": [],
    "key_auths": [
      [
        "STM5D1ZCtab5A5DzioThZSGKCASa4QuYXsegaFYr4E2edcKa85ciP",
        1
      ]
    ],
    "weight_threshold": 1
  },
  "memo": "STM6P3LvVZjSRnWetfbWcXeEcF6smw5xpW87s43oEd21Lea53ovPb"
}

Witness Votes

0 / 30
No active witness votes.
[]