journeyviz 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 00ee3821859c87a8c2a049d1ff60258ad6a5dfcd82e24ce3a8dd71dba9c1eead
4
+ data.tar.gz: ab015af88e3d0fb7de33f0f6a9a2b427f1a87ddc8ab034dfce18438a1d0a8298
5
+ SHA512:
6
+ metadata.gz: fb8ac7208e4bddc29cf4b7567d74e5abb2a5bfa33e13421c5eac58bedee9637a600eecfec1eae7229ded8a7530eb0a84a7fed14146c53f6d686eb9eb07a50e90
7
+ data.tar.gz: ee35eadfa9a2ab5f8e94f454661beda75972b1c95b809e412d776402b8103debb3f11026975e0bf780c3531e44127e1dea295083b241160d6c18a483a2789054
@@ -0,0 +1,30 @@
1
+ name: Ruby Gem
2
+
3
+ on:
4
+ push:
5
+ branches: [ master ]
6
+ pull_request:
7
+ branches: [ master ]
8
+
9
+ jobs:
10
+ build:
11
+ name: Build + Publish
12
+ runs-on: ubuntu-latest
13
+
14
+ steps:
15
+ - uses: actions/checkout@v2
16
+ - name: Set up Ruby 2.6
17
+ uses: actions/setup-ruby@v1
18
+ with:
19
+ version: 2.6.x
20
+
21
+ - name: Publish to RubyGems
22
+ run: |
23
+ mkdir -p $HOME/.gem
24
+ touch $HOME/.gem/credentials
25
+ chmod 0600 $HOME/.gem/credentials
26
+ printf -- "---\n:rubygems_api_key: ${GEM_HOST_API_KEY}\n" > $HOME/.gem/credentials
27
+ gem build *.gemspec
28
+ gem push *.gem
29
+ env:
30
+ GEM_HOST_API_KEY: ${{secrets.RUBYGEMS_AUTH_TOKEN}}
@@ -0,0 +1,39 @@
1
+ name: CI
2
+
3
+ on: [push, pull_request]
4
+
5
+ jobs:
6
+ main:
7
+ name: >-
8
+ ${{ matrix.os }} ${{ matrix.ruby }}
9
+ runs-on: ${{ matrix.os }}-latest
10
+ strategy:
11
+ fail-fast: false
12
+ matrix:
13
+ os: [ ubuntu, macos, windows ]
14
+ ruby: [ 2.4, 2.5, 2.6, 2.7, head ]
15
+ include:
16
+ - { os: windows, ruby: mingw }
17
+ exclude:
18
+ - { os: windows, ruby: head }
19
+
20
+ steps:
21
+ - name: windows misc
22
+ if: matrix.os == 'windows'
23
+ run: |
24
+ # set TMPDIR, git core.autocrlf
25
+ echo "::set-env name=TMPDIR::$env:RUNNER_TEMP"
26
+ git config --system core.autocrlf false
27
+ - name: checkout
28
+ uses: actions/checkout@v2
29
+ - name: set up Ruby
30
+ uses: ruby/setup-ruby@v1
31
+ with:
32
+ ruby-version: ${{ matrix.ruby }}
33
+
34
+ - name: install dependencies
35
+ run: bundle install --jobs 3 --retry 3
36
+ - name: spec
37
+ run: bundle exec rspec
38
+ - name: linter
39
+ run: bundle exec rubocop
@@ -0,0 +1,11 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+ .byebug_history
10
+ # rspec failure tracking
11
+ .rspec_status
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
@@ -0,0 +1,35 @@
1
+ Layout/LineLength:
2
+ Max: 120
3
+
4
+ # TODO: Enable documentation
5
+ Style/Documentation:
6
+ Enabled: false
7
+
8
+ Lint/RaiseException:
9
+ Enabled: true
10
+
11
+ Lint/StructNewOverride:
12
+ Enabled: true
13
+
14
+ Style/HashEachMethods:
15
+ Enabled: true
16
+
17
+ Style/HashTransformKeys:
18
+ Enabled: true
19
+
20
+ Style/HashTransformValues:
21
+ Enabled: true
22
+
23
+ Style/AsciiComments:
24
+ Enabled: false
25
+
26
+ Metrics/BlockLength:
27
+ Exclude:
28
+ - spec/**/*
29
+
30
+ Lint/AmbiguousBlockAssociation:
31
+ Exclude:
32
+ - spec/**/*
33
+
34
+ Layout/EndOfLine:
35
+ EnforcedStyle: lf
@@ -0,0 +1,7 @@
1
+ ---
2
+ sudo: false
3
+ language: ruby
4
+ cache: bundler
5
+ rvm:
6
+ - 2.6.4
7
+ before_install: gem install bundler -v 2.0.2
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ # Specify your gem's dependencies in journeyviz.gemspec
6
+ gemspec
@@ -0,0 +1,65 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ journeyviz (0.1.0)
5
+ rack-app
6
+
7
+ GEM
8
+ remote: https://rubygems.org/
9
+ specs:
10
+ ast (2.4.0)
11
+ byebug (11.1.3)
12
+ diff-lcs (1.3)
13
+ docile (1.3.2)
14
+ jaro_winkler (1.5.4)
15
+ parallel (1.19.1)
16
+ parser (2.7.1.0)
17
+ ast (~> 2.4.0)
18
+ rack (2.2.3)
19
+ rack-app (7.6.4)
20
+ rack
21
+ rainbow (3.0.0)
22
+ rake (13.0.1)
23
+ rexml (3.2.4)
24
+ rspec (3.9.0)
25
+ rspec-core (~> 3.9.0)
26
+ rspec-expectations (~> 3.9.0)
27
+ rspec-mocks (~> 3.9.0)
28
+ rspec-core (3.9.1)
29
+ rspec-support (~> 3.9.1)
30
+ rspec-expectations (3.9.1)
31
+ diff-lcs (>= 1.2.0, < 2.0)
32
+ rspec-support (~> 3.9.0)
33
+ rspec-mocks (3.9.1)
34
+ diff-lcs (>= 1.2.0, < 2.0)
35
+ rspec-support (~> 3.9.0)
36
+ rspec-support (3.9.2)
37
+ rubocop (0.81.0)
38
+ jaro_winkler (~> 1.5.1)
39
+ parallel (~> 1.10)
40
+ parser (>= 2.7.0.1)
41
+ rainbow (>= 2.2.2, < 4.0)
42
+ rexml
43
+ ruby-progressbar (~> 1.7)
44
+ unicode-display_width (>= 1.4.0, < 2.0)
45
+ ruby-progressbar (1.10.1)
46
+ simplecov (0.18.5)
47
+ docile (~> 1.1)
48
+ simplecov-html (~> 0.11)
49
+ simplecov-html (0.12.2)
50
+ unicode-display_width (1.7.0)
51
+
52
+ PLATFORMS
53
+ ruby
54
+
55
+ DEPENDENCIES
56
+ bundler (~> 2.0)
57
+ byebug
58
+ journeyviz!
59
+ rake (~> 13.0)
60
+ rspec (~> 3.0)
61
+ rubocop (~> 0.81.0)
62
+ simplecov
63
+
64
+ BUNDLED WITH
65
+ 2.0.2
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2020 Gabriel Teles
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
@@ -0,0 +1,99 @@
1
+ # Journeyviz
2
+
3
+ Journeyviz is a journey visualization gem. It defines journey as code so it can be versioned together with changes.
4
+
5
+ When defined, you can view your journey through a [rack](https://github.com/rack/rack) application.
6
+
7
+ Supports Ruby 2.6.x officially.
8
+
9
+ ## Installation
10
+
11
+ Add this line to your application's Gemfile:
12
+
13
+ ```ruby
14
+ gem 'journeyviz'
15
+ ```
16
+
17
+ And then execute:
18
+
19
+ $ bundle
20
+
21
+ Or install it yourself as:
22
+
23
+ $ gem install journeyviz
24
+
25
+ ## Usage
26
+
27
+
28
+ ### Defining the journey
29
+
30
+ Start by defining a journey. It is consisted of for elements: blocks, screens, actions and transitions.
31
+
32
+ Let's start from screens:
33
+
34
+ ```ruby
35
+ # This is the journey definition block
36
+ Journeyviz.configure do |journey|
37
+ # Here we're defining a screen called landing page
38
+ journey.screen :landing_page
39
+ end
40
+ ```
41
+
42
+ Now supose our landing page has a share button and we'd like to include it into our journey. We should define an action to represent it.
43
+
44
+ ```ruby
45
+ Journeyviz.configure do |journey|
46
+ journey.screen :landing_page do |landing|
47
+ # Landing has an action called :share
48
+ landing.action :share
49
+ end
50
+ end
51
+ ```
52
+
53
+ Imagine that the landing page also has an login form that sends the user to a logged-in area, into a dashboard page. Now we're going to use blocks and transitions.
54
+
55
+ ```ruby
56
+ Journeyviz.configure do |journey|
57
+ journey.screen :landing_page do |landing|
58
+ landing.action :share
59
+ # Define an action `login` that transitions the user to dashboard page
60
+ # %i[logged dashboard] is the path to that screen. It includes every
61
+ # block and finally the screen name.
62
+ landing.action :login, transition: %i[logged dashboard]
63
+ end
64
+
65
+ # Defining the logged area
66
+ journey.block :logged do |logged_area|
67
+ # Dashboard inside logged area
68
+ logged_area.screen :dashboard
69
+
70
+ # Blocks can have blocks, you can call `logged_area.block :sublock` how many
71
+ # times you want.
72
+ end
73
+ end
74
+ ```
75
+
76
+ ### Visualizing journey
77
+
78
+ With journey definition loaded, you just have to mount the journeyviz server into your
79
+ rack application with `mount` command:
80
+
81
+ ```ruby
82
+ mount Journeyviz::Server => '/journeyviz'
83
+ ```
84
+
85
+ No just go to `/journeyviz` path on your application!
86
+
87
+ ## Development
88
+
89
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
90
+
91
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
92
+
93
+ ## Contributing
94
+
95
+ Bug reports and pull requests are welcome on GitHub at https://github.com/gabteles/journeyviz.
96
+
97
+ ## License
98
+
99
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
4
+ require 'rspec/core/rake_task'
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ task default: :spec
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'bundler/setup'
5
+ require 'journeyviz'
6
+
7
+ # You can add fixtures and/or initialization code here to make experimenting
8
+ # with your gem easier. You can also use a different console, if you like.
9
+
10
+ # (If you use this, don't forget to add pry to your Gemfile!)
11
+ # require "pry"
12
+ # Pry.start
13
+
14
+ require 'irb'
15
+ IRB.start(__FILE__)
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ lib = File.expand_path('lib', __dir__)
4
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
+ require 'journeyviz/version'
6
+
7
+ Gem::Specification.new do |spec|
8
+ spec.name = 'journeyviz'
9
+ spec.version = Journeyviz::VERSION
10
+ spec.authors = ['Gabriel Teles']
11
+ spec.email = ['gabz.teles@gmail.com']
12
+
13
+ spec.summary = 'Journey visualization made easy'
14
+ spec.description = 'Define your user journey as code and track it historically and in realtime'
15
+ spec.homepage = 'https://github.com/gabteles/journeyviz'
16
+ spec.license = 'MIT'
17
+
18
+ spec.metadata['homepage_uri'] = spec.homepage
19
+ spec.metadata['source_code_uri'] = spec.homepage
20
+ spec.metadata['changelog_uri'] = "#{spec.homepage}/CHANGELOG.md"
21
+
22
+ # Specify which files should be added to the gem when it is released.
23
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
24
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
25
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
26
+ end
27
+ spec.bindir = 'exe'
28
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
29
+ spec.require_paths = ['lib']
30
+
31
+ spec.add_development_dependency 'bundler', '~> 2.0'
32
+ spec.add_development_dependency 'byebug'
33
+ spec.add_development_dependency 'rake', '~> 13.0'
34
+ spec.add_development_dependency 'rspec', '~> 3.0'
35
+ spec.add_development_dependency 'rubocop', '~> 0.81.0'
36
+ spec.add_development_dependency 'simplecov'
37
+ spec.add_dependency 'rack-app'
38
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'journeyviz/version'
4
+ require 'journeyviz/server'
5
+ require 'journeyviz/journey'
6
+
7
+ module Journeyviz
8
+ class Error < StandardError; end
9
+ class InvalidNameError < Error; end
10
+ class DuplicatedDefinition < Error; end
11
+ class InvalidTransition < Error; end
12
+
13
+ class << self
14
+ def configure(&block)
15
+ @journey = Journey.new
16
+ block.call(journey)
17
+ journey.validate!
18
+ end
19
+
20
+ def identify(user_id)
21
+ context[:user_id] = user_id
22
+ end
23
+
24
+ def visit(screen)
25
+ # TODO
26
+ # screen/day/:day/visits ++
27
+ # screen/week/:week/visits ++
28
+ # screen/month/:month/visits ++
29
+ # screen/quarter/:quarter/visits ++
30
+ end
31
+
32
+ def act(action)
33
+ # TODO
34
+ # :action/day/:day ++
35
+ # :action/week/:week ++
36
+ # :action/month/:month ++
37
+ # :action/quarter/:quarter ++
38
+ end
39
+
40
+ def context
41
+ Thread.current[:journeyviz_context] ||= {}
42
+ end
43
+
44
+ def journey
45
+ @journey ||= Journey.new
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'journeyviz/normalized_name'
4
+
5
+ module Journeyviz
6
+ class Action
7
+ include NormalizedName
8
+ attr_reader :screen
9
+
10
+ def initialize(name, screen, transition: nil)
11
+ assign_normalize_name(name)
12
+ @screen = screen
13
+ @transition = transition
14
+ end
15
+
16
+ def transition
17
+ case @transition
18
+ when Symbol then find_screen_by_name(@transition)
19
+ when Array then find_screen_by_full_qualifier(@transition)
20
+ end
21
+ end
22
+
23
+ def raw_transition
24
+ @transition
25
+ end
26
+
27
+ private
28
+
29
+ def find_screen_by_name(screen_name)
30
+ qualifier = screen.full_qualifier
31
+
32
+ (qualifier.size - 1).downto(0) do |len|
33
+ found_screen = find_screen_by_full_qualifier(qualifier[0, len] + [screen_name])
34
+ return found_screen if found_screen
35
+ end
36
+
37
+ nil
38
+ end
39
+
40
+ def find_screen_by_full_qualifier(qualifier)
41
+ screen.root_scope.find_screen(qualifier)
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'journeyviz/normalized_name'
4
+ require 'journeyviz/node_group'
5
+ require 'journeyviz/graphable'
6
+ require 'journeyviz/scopable'
7
+
8
+ module Journeyviz
9
+ class Block
10
+ include NormalizedName
11
+ include NodeGroup
12
+ include Graphable
13
+ include Scopable[:name]
14
+
15
+ def initialize(name, scope = nil)
16
+ assign_normalize_name(name)
17
+ assign_scope(scope)
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'journeyviz/graphable/externals'
4
+ require 'journeyviz/graphable/transitions'
5
+
6
+ module Journeyviz
7
+ module Graphable
8
+ include Externals
9
+ include Transitions
10
+
11
+ def graph
12
+ defs = [
13
+ *graph_screens,
14
+ *graph_blocks,
15
+ *graph_inputs,
16
+ *graph_outputs,
17
+ *graph_transitions,
18
+ *graph_styles
19
+ ].join("\n")
20
+
21
+ "graph LR\n#{defs}"
22
+ end
23
+
24
+ private
25
+
26
+ def graph_screens
27
+ (@screens || []).map { |screen| "#{graph_id(screen)}[#{screen.name}]:::screen" }
28
+ end
29
+
30
+ def graph_blocks
31
+ (@blocks || []).map { |block| "#{graph_id(block)}[#{block.name}]:::block" }
32
+ end
33
+
34
+ def graph_styles
35
+ [
36
+ 'classDef screen fill:#373496,stroke:#373496,stroke-width:2px,color:#fff',
37
+ 'classDef transition fill:#fff,stroke:#373496,stroke-width:2px,color:#373496,stroke-dasharray: 5, 5',
38
+ 'classDef block fill:#963734,color:#fff'
39
+ ]
40
+ end
41
+
42
+ def graph_id(node)
43
+ case node
44
+ when Journeyviz::Screen then screen_id(node)
45
+ when Journeyviz::Block then block_id(node)
46
+ when Journeyviz::Action then action_id(node)
47
+ end
48
+ end
49
+
50
+ def screen_id(node)
51
+ "screen_#{node.full_qualifier.join('_')}"
52
+ end
53
+
54
+ def block_id(node)
55
+ "block_#{node.full_qualifier.join('_')}"
56
+ end
57
+
58
+ def action_id(node)
59
+ from_id = graph_id(node.screen)
60
+ to_id = graph_target(node)
61
+ "transition_#{from_id}_#{node.name}_#{to_id}"
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Journeyviz
4
+ module Graphable
5
+ module Externals
6
+ private
7
+
8
+ def graph_inputs
9
+ return '' unless defined?(root_scope)
10
+
11
+ inputs = self.inputs.map { |screen| screen.full_scope + [screen] }
12
+
13
+ return '' if inputs.empty?
14
+
15
+ <<~INPUTS
16
+ subgraph inputs
17
+ #{graph_external_chain(inputs, 'input')}
18
+ end
19
+ INPUTS
20
+ end
21
+
22
+ def graph_outputs
23
+ outputs = self.outputs.map { |screen| screen.full_scope + [screen] }
24
+ return '' if outputs.empty?
25
+
26
+ <<~OUTPUTS
27
+ subgraph outputs
28
+ #{graph_external_chain(outputs, 'output')}
29
+ end
30
+ OUTPUTS
31
+ end
32
+
33
+ def graph_external_chain(nodes, sufix)
34
+ nodes
35
+ .group_by(&:first)
36
+ .flat_map { |parent, chain| graph_external_chain_node(parent, chain, sufix) }
37
+ .join("\n")
38
+ end
39
+
40
+ def graph_external_chain_node(parent, chain, sufix)
41
+ chain_without_parent = chain.map { |subchain| subchain[1..-1] }
42
+
43
+ if parent.is_a?(Journeyviz::Screen)
44
+ [graph_external_screen(parent, sufix)] + graph_external_transitions(parent)
45
+ else
46
+ graph_external_block(parent, chain_without_parent, sufix)
47
+ end
48
+ end
49
+
50
+ def graph_external_screen(screen, sufix)
51
+ "#{sufix}_#{graph_id(screen)}[#{screen.name}]:::external_screen"
52
+ end
53
+
54
+ def graph_external_transitions(screen)
55
+ screen
56
+ .actions
57
+ .select { |action| screens.include?(action.transition) }
58
+ .map { |action| "#{graph_id(action)}(#{action.name}):::transition" }
59
+ end
60
+
61
+ def graph_external_block(block, chain, sufix)
62
+ definition = graph_external_chain(chain, sufix)
63
+ return definition if block.is_a?(Journeyviz::Journey)
64
+
65
+ <<~SUBGRAPH
66
+ subgraph #{sufix}_#{graph_id(block)}[#{block.name}]
67
+ #{definition}
68
+ end
69
+ SUBGRAPH
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Journeyviz
4
+ module Graphable
5
+ module Transitions
6
+ private
7
+
8
+ def graph_transitions
9
+ [
10
+ *graph_screen_transitions,
11
+ *graph_block_transitions,
12
+ *graph_external_inputs
13
+ ]
14
+ end
15
+
16
+ def graph_transition_node(action)
17
+ from_id = graph_id(action.screen)
18
+ to_id = graph_target(action)
19
+ "transition_#{from_id}_#{action.name}_#{to_id}(#{action.name}):::transition"
20
+ end
21
+
22
+ def graph_screen_transitions
23
+ @screens
24
+ .flat_map(&:actions)
25
+ .select(&:transition)
26
+ .flat_map do |action|
27
+ [graph_transition_node(action), graph_screen_transition(action)]
28
+ end
29
+ end
30
+
31
+ def graph_screen_transition(action)
32
+ "#{graph_id(action.screen)} --- #{graph_id(action)} --> #{graph_target(action)}"
33
+ end
34
+
35
+ def graph_target(action)
36
+ target = action.transition
37
+
38
+ return "output_#{graph_id(target)}" if outputs.include?(target)
39
+
40
+ direct_children = (@blocks || []) + (@screens || [])
41
+ target = target.scope until direct_children.include?(target)
42
+ graph_id(target)
43
+ end
44
+
45
+ def graph_external_inputs
46
+ inputs
47
+ .flat_map(&:actions)
48
+ .select { |action| screens.include?(action.transition) }
49
+ .map { |action| "input_#{graph_screen_transition(action)}" }
50
+ end
51
+
52
+ def graph_block_transitions
53
+ @blocks.flat_map do |block|
54
+ targets = block.outputs
55
+ screen_targets = @screens & targets
56
+ block_targets = @blocks.select do |target_block|
57
+ targets.any? { |target| target_block.screens.include?(target) }
58
+ end
59
+
60
+ (screen_targets + block_targets).map do |target|
61
+ "#{graph_id(block)} --> #{graph_id(target)}"
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'journeyviz/node_group'
4
+ require 'journeyviz/graphable'
5
+ require 'journeyviz/block'
6
+
7
+ module Journeyviz
8
+ class Journey
9
+ include NodeGroup
10
+ include Graphable
11
+
12
+ def initialize
13
+ @blocks = []
14
+ end
15
+
16
+ def validate!
17
+ screens.each do |screen|
18
+ screen.actions.each { |action| validate_action(action) }
19
+ end
20
+ end
21
+
22
+ private
23
+
24
+ def validate_action(action)
25
+ return if action.transition || !action.raw_transition
26
+
27
+ message = "Action #{action.name.inspect} "
28
+ message += "on screen #{action.screen.full_qualifier.inspect} "
29
+ message += "has invalid transition: #{action.raw_transition.inspect}"
30
+ raise(Journeyviz::InvalidTransition, message)
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'journeyviz/screen'
4
+
5
+ module Journeyviz
6
+ autoload :Block, 'journeyviz/block'
7
+
8
+ module NodeGroup
9
+ def block(name, &definition)
10
+ block = Block.new(name, self)
11
+
12
+ @blocks ||= []
13
+ if @blocks.any? { |defined_block| block.name == defined_block.name }
14
+ raise DuplicatedDefinition, "Duplicated block name: #{name}"
15
+ end
16
+
17
+ @blocks.push(block)
18
+ definition.call(block)
19
+ block
20
+ end
21
+
22
+ def blocks(include_children: false)
23
+ @blocks ||= []
24
+
25
+ if include_children
26
+ @blocks.flat_map { |block| [block] + block.blocks(include_children: true) }
27
+ else
28
+ @blocks
29
+ end
30
+ end
31
+
32
+ def screen(name)
33
+ @screens ||= []
34
+ screen = Screen.new(name, self)
35
+
36
+ if @screens.any? { |defined_screen| screen.name == defined_screen.name }
37
+ raise DuplicatedDefinition, "Duplicated screen name: #{name}"
38
+ end
39
+
40
+ @screens.push(screen)
41
+ yield screen if block_given?
42
+ screen
43
+ end
44
+
45
+ def screens
46
+ @screens ||= []
47
+ @screens + blocks.flat_map(&:screens)
48
+ end
49
+
50
+ def find_screen(qualifier)
51
+ screens.find { |screen| screen.full_qualifier == qualifier }
52
+ end
53
+
54
+ def inputs
55
+ options = defined?(root_scope) && root_scope ? root_scope.screens : []
56
+ external_screens = options - screens
57
+ self_screens = screens
58
+ external_screens.select do |screen|
59
+ screen.actions.any? do |action|
60
+ self_screens.include?(action.transition)
61
+ end
62
+ end
63
+ end
64
+
65
+ def outputs
66
+ screens
67
+ .flat_map(&:actions)
68
+ .map(&:transition)
69
+ .compact
70
+ .reject { |screen| screens.include?(screen) }
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Journeyviz
4
+ module NormalizedName
5
+ attr_reader :name
6
+
7
+ private
8
+
9
+ def assign_normalize_name(name)
10
+ if !name.is_a?(String) && !name.is_a?(Symbol) || name.size <= 0
11
+ raise InvalidNameError, "Invalid name given: #{name.inspect}"
12
+ end
13
+
14
+ @name = name.to_sym
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Journeyviz
4
+ Scopable = proc do |qualified_by|
5
+ Module.new do
6
+ attr_reader :scope
7
+
8
+ def assign_scope(scope)
9
+ @scope = scope
10
+ end
11
+
12
+ def root_scope
13
+ current_scope = self
14
+ current_scope = current_scope.scope until !current_scope.respond_to?(:scope) || current_scope.scope.nil?
15
+ current_scope
16
+ end
17
+
18
+ def full_scope
19
+ current_scope = scope
20
+ chain = [current_scope]
21
+
22
+ until current_scope.nil? || !current_scope.respond_to?(:scope)
23
+ current_scope = current_scope.scope
24
+ chain.unshift(current_scope)
25
+ end
26
+
27
+ chain
28
+ end
29
+
30
+ define_method(:full_qualifier) do
31
+ base_qualifier = scope&.respond_to?(:full_qualifier) ? scope.full_qualifier : []
32
+ [*base_qualifier, send(qualified_by)].compact
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'journeyviz/action'
4
+ require 'journeyviz/normalized_name'
5
+ require 'journeyviz/scopable'
6
+
7
+ module Journeyviz
8
+ class Screen
9
+ include NormalizedName
10
+ include Scopable[:name]
11
+ attr_reader :actions, :scope
12
+
13
+ def initialize(name, scope = nil)
14
+ assign_normalize_name(name)
15
+ @actions = []
16
+ assign_scope(scope)
17
+ end
18
+
19
+ def action(name, **params)
20
+ action = Action.new(name, self, **params)
21
+
22
+ if actions.any? { |defined_action| action.name == defined_action.name }
23
+ raise DuplicatedDefinition, "Duplicated action name: #{name}"
24
+ end
25
+
26
+ @actions << action
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rack/app'
4
+
5
+ module Journeyviz
6
+ class Server < Rack::App
7
+ get '/' do
8
+ block_path = params['block'] || ''
9
+ @block = block_path.split('_').reduce(Journeyviz.journey) do |current, step|
10
+ current.blocks.find { |block| block.name == step.to_sym }
11
+ end
12
+
13
+ path = File.expand_path('server/index.html.erb', __dir__)
14
+ ERB.new(File.read(path)).result(binding)
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,100 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta http-equiv="X-UA-Compatible" content="IE=edge">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0">
7
+ <title>Journeyviz</title>
8
+ <link rel="icon" type="image/png" href="data:image/png;base64,iVBORw0KGgo=">
9
+ <link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">
10
+ <link rel="stylesheet" href="https://code.getmdl.io/1.3.0/material.indigo-pink.min.css">
11
+ <style>
12
+ .mermaid {
13
+ margin: 2em auto;
14
+ display: none;
15
+ max-width: 100%;
16
+ overflow: auto;
17
+ border-radius: 5px;
18
+ background: #ddd;
19
+ }
20
+
21
+ .mermaid[data-processed] {
22
+ display: block;
23
+ }
24
+
25
+ .demo-drawer {
26
+ border: none;
27
+ }
28
+ /* iOS Safari specific workaround */
29
+ .demo-drawer .mdl-menu__container {
30
+ z-index: -1;
31
+ }
32
+ .demo-drawer .demo-navigation {
33
+ z-index: -2;
34
+ }
35
+ .demo-navigation {
36
+ flex-grow: 1;
37
+ }
38
+ .demo-layout .mdl-layout__header .mdl-layout__drawer-button {
39
+ color: rgba(0, 0, 0, 0.54);
40
+ }
41
+ .demo-layout .demo-navigation .mdl-navigation__link {
42
+ display: flex !important;
43
+ flex-direction: row;
44
+ align-items: center;
45
+ color: rgba(255, 255, 255, 0.56);
46
+ font-weight: 500;
47
+ }
48
+ .demo-layout .demo-navigation .mdl-navigation__link:hover {
49
+ background-color: #00BCD4;
50
+ color: #37474F;
51
+ }
52
+ .demo-navigation hr {
53
+ border-color: rgba(0,0,0,0.34);
54
+ }
55
+ .demo-navigation .mdl-navigation__link .material-icons {
56
+ font-size: 24px;
57
+ color: rgba(255, 255, 255, 0.56);
58
+ margin-right: 32px;
59
+ }
60
+ </style>
61
+ </head>
62
+
63
+ <body>
64
+ <div class="demo-layout mdl-layout mdl-js-layout mdl-layout--fixed-drawer mdl-layout--fixed-header">
65
+ <header class="demo-header mdl-layout__header mdl-color--grey-100 mdl-color-text--grey-600">
66
+ <div class="mdl-layout__header-row">
67
+ <span class="mdl-layout-title">
68
+ <%= @block.respond_to?(:name) ? "#{@block.name.to_s.split('_').collect(&:capitalize).join(' ')} / Journeyviz" : 'Journeyviz' %>
69
+ </span>
70
+ </div>
71
+ </header>
72
+
73
+ <div class="demo-drawer mdl-layout__drawer mdl-color--blue-grey-900 mdl-color-text--blue-grey-50">
74
+ <nav class="demo-navigation mdl-navigation mdl-color--blue-grey-800">
75
+ <a class="mdl-navigation__link" href="?">
76
+ <i class="mdl-color-text--blue-grey-400 material-icons" role="presentation">home</i>
77
+ <span>Journey</span>
78
+ </a>
79
+ <hr/>
80
+ <% @block.blocks.each do |block| %>
81
+ <a class="mdl-navigation__link" href="?block=<%= block.full_qualifier.join('_') %>">
82
+ <i class="mdl-color-text--blue-grey-400 material-icons" role="presentation">layers</i>
83
+ <span><%= block.name.to_s.split('_').collect(&:capitalize).join(' ') %></span>
84
+ </a>
85
+ <% end %>
86
+ </nav>
87
+ </div>
88
+
89
+ <main class="mdl-layout__content mdl-color--grey-100">
90
+ <div class="mdl-grid demo-content">
91
+ <div class="mermaid"><%= @block.graph %></div>
92
+ </div>
93
+ </main>
94
+ </div>
95
+
96
+ <script defer src="https://code.getmdl.io/1.3.0/material.min.js"></script>
97
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/mermaid/8.4.8/mermaid.min.js"></script>
98
+ <script>mermaid.initialize({ startOnLoad: true });</script>
99
+ </body>
100
+ </html>
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Journeyviz
4
+ VERSION = '0.1.0'
5
+ end
metadata ADDED
@@ -0,0 +1,172 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: journeyviz
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Gabriel Teles
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2020-06-30 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '2.0'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '2.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: byebug
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rake
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '13.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '13.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rspec
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '3.0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '3.0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rubocop
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: 0.81.0
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: 0.81.0
83
+ - !ruby/object:Gem::Dependency
84
+ name: simplecov
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: rack-app
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :runtime
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ description: Define your user journey as code and track it historically and in realtime
112
+ email:
113
+ - gabz.teles@gmail.com
114
+ executables: []
115
+ extensions: []
116
+ extra_rdoc_files: []
117
+ files:
118
+ - ".github/workflows/gem-push.yml"
119
+ - ".github/workflows/tests.yml"
120
+ - ".gitignore"
121
+ - ".rspec"
122
+ - ".rubocop.yml"
123
+ - ".travis.yml"
124
+ - Gemfile
125
+ - Gemfile.lock
126
+ - LICENSE.txt
127
+ - README.md
128
+ - Rakefile
129
+ - bin/console
130
+ - bin/setup
131
+ - journeyviz.gemspec
132
+ - lib/journeyviz.rb
133
+ - lib/journeyviz/action.rb
134
+ - lib/journeyviz/block.rb
135
+ - lib/journeyviz/graphable.rb
136
+ - lib/journeyviz/graphable/externals.rb
137
+ - lib/journeyviz/graphable/transitions.rb
138
+ - lib/journeyviz/journey.rb
139
+ - lib/journeyviz/node_group.rb
140
+ - lib/journeyviz/normalized_name.rb
141
+ - lib/journeyviz/scopable.rb
142
+ - lib/journeyviz/screen.rb
143
+ - lib/journeyviz/server.rb
144
+ - lib/journeyviz/server/index.html.erb
145
+ - lib/journeyviz/version.rb
146
+ homepage: https://github.com/gabteles/journeyviz
147
+ licenses:
148
+ - MIT
149
+ metadata:
150
+ homepage_uri: https://github.com/gabteles/journeyviz
151
+ source_code_uri: https://github.com/gabteles/journeyviz
152
+ changelog_uri: https://github.com/gabteles/journeyviz/CHANGELOG.md
153
+ post_install_message:
154
+ rdoc_options: []
155
+ require_paths:
156
+ - lib
157
+ required_ruby_version: !ruby/object:Gem::Requirement
158
+ requirements:
159
+ - - ">="
160
+ - !ruby/object:Gem::Version
161
+ version: '0'
162
+ required_rubygems_version: !ruby/object:Gem::Requirement
163
+ requirements:
164
+ - - ">="
165
+ - !ruby/object:Gem::Version
166
+ version: '0'
167
+ requirements: []
168
+ rubygems_version: 3.0.3
169
+ signing_key:
170
+ specification_version: 4
171
+ summary: Journey visualization made easy
172
+ test_files: []