journeyviz 0.1.0

This diff has not been reviewed by any users.
Log in in order to be able to vote.
@@ -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: []