SlideShare a Scribd company logo
1 of 180
Download to read offline
Who makes the best
     Asado?
What country has the
 best footballers?
Argentina!
river
river




Nacional B???
NOOOOOOOO!!!!
Aaron Patterson
@tenderlove
Ruby core team
Rails core team
! WARNING !
ZOMG!
We are in Argentina!
José   Yo
WWFMD?
me gusta
¿Por qué Maria?
THANKS!!
Resource
Management
Path Management
Move bet ween theory
   and practice




 Pattern
Matching
Graphviz
 http://graphviz.org
greeting
hello              world
graph.dot

digraph nfa {
  rankdir=LR;

    world [shape = doublecircle];
    hello [shape = circle];
    hello -> world [label="greeting"];
}
graph.dot
digraph nfa {
  rankdir=LR;

    world [shape = doublecircle];
    hello [shape = circle];
    goodbye [shape = circle];

    hello -> world [label="greeting"];
    goodbye -> world
}
hello
          greeting

                     world


goodbye
/articles(.:format)

    /       articles       .       (?-mix:[^./?]+)
0       1              2       3                     5
                           /

                                        new
                               4                     6   /articles/new(.:format)
Journey
Journey
Yes, it's named after the '70s rock sensation
Provides
  1: URL generation
  2: Path Recognition
  3: Route parsing




What is a router?
Why a new router?
Maintenance
LOC
3000



2250



1500



 750



   0
       Journey   Journey-2   Rack-Mount
Known algorithms
References

• Compilers: Principles, Techniques, &
  Tools (Aho, Lam, Sethi, Ullman)

• Intro to Formal Languages and
  Automata (Linz)
Predictability
CPU Time
Memory Usage
Current Performance
            rack-mount                  Journey




   url generation    path recognition       route parsing
me gusta
Patterns
Why patterns matter
Regular Expressions
       /om(g)!/
Rails Routes
resource :articles
/articles(.:format)
/articles/new(.:format)
/articles/:id/edit(.:format)
/articles/:id(.:format)
Parens don't capture
:whatever => /([^./?]+)/
/articles(.:format)

//articles(?:.([^./?]+))?$/
//articles(?:.([^./?]+))?$/

//articles/new(?:.([^./?]+))?$/

//articles/([^./?]+)/edit(?:.([^./?]+))?$/

//articles/([^./?]+)(?:.([^./?]+))?$/
GET /articles/new

//articles(?:.([^./?]+))?$/

//articles/new(?:.([^./?]+))?$/

//articles/([^./?]+)/edit(?:.([^./?]+))?$/

//articles/([^./?]+)(?:.([^./?]+))?$/
GET /articles/new

//articles(?:.([^./?]+))?$/

//articles/new(?:.([^./?]+))?$/

//articles/([^./?]+)/edit(?:.([^./?]+))?$/

//articles/([^./?]+)(?:.([^./?]+))?$/
GET /articles/new

//articles(?:.([^./?]+))?$/

//articles/new(?:.([^./?]+))?$/

//articles/([^./?]+)/edit(?:.([^./?]+))?$/

//articles/([^./?]+)(?:.([^./?]+))?$/



              200 OK!
//articles(?:.([^./?]+))?$/

//articles/new(?:.([^./?]+))?$/

//articles/([^./?]+)/edit(?:.([^./?]+))?$/

//articles/([^./?]+)(?:.([^./?]+))?$/
GET /foos/new

//articles(?:.([^./?]+))?$/

//articles/new(?:.([^./?]+))?$/

//articles/([^./?]+)/edit(?:.([^./?]+))?$/

//articles/([^./?]+)(?:.([^./?]+))?$/
GET /foos/new

//articles(?:.([^./?]+))?$/

//articles/new(?:.([^./?]+))?$/

//articles/([^./?]+)/edit(?:.([^./?]+))?$/

//articles/([^./?]+)(?:.([^./?]+))?$/
GET /foos/new

//articles(?:.([^./?]+))?$/

//articles/new(?:.([^./?]+))?$/

//articles/([^./?]+)/edit(?:.([^./?]+))?$/

//articles/([^./?]+)(?:.([^./?]+))?$/
GET /foos/new

//articles(?:.([^./?]+))?$/

//articles/new(?:.([^./?]+))?$/

//articles/([^./?]+)/edit(?:.([^./?]+))?$/

//articles/([^./?]+)(?:.([^./?]+))?$/
GET /foos/new

//articles(?:.([^./?]+))?$/

//articles/new(?:.([^./?]+))?$/

//articles/([^./?]+)/edit(?:.([^./?]+))?$/

//articles/([^./?]+)(?:.([^./?]+))?$/
GET /foos/new

//articles(?:.([^./?]+))?$/

//articles/new(?:.([^./?]+))?$/

//articles/([^./?]+)/edit(?:.([^./?]+))?$/

//articles/([^./?]+)(?:.([^./?]+))?$/



         404 Not Found
How long does this
      take?
r = routes.length
x = regexp compare
O(r ⨉ x)
Can we do better?
Parse Trees


     Automata


Finite State Machines
Parse Trees
Parsing regexp
(a|b)*abb
○   Describe node types



  ○ b

○ b

a *

  ()

  |

 a b
Parsing rails routes
Journey::Parser
parser = Journey::Parser.new
ast    = parser.parse '/articles(.:format)'

puts ast.to_dot
○

      ○        ()

/   articles    ○

           .        :format
To String!


parser = Journey::Parser.new
ast    = parser.parse '/articles(.:format)'
puts ast.to_s
puts ast.to_regexp
OR AST
parser = Journey::Parser.new

trees = [
  '/articles(.:format)',
  '/articles/new(.:format)',
].map { |s| parser.parse s }

ast = Journey::Nodes::Or.new trees

puts ast.to_dot
|

                 ○                   ○

      ○          ()                  ○         ()

/   articles     ○               ○       new    ○

       .       :format           ○       /          .   :format

                         /       articles
SECRET FEATURE
parser = Journey::Parser.new
ast = parser.parse '/articles|books(.:format)'
SECRET FEATURE
parser = Journey::Parser.new
                            RE T!
                        EC
ast = parser.parse '/articles|books(.:format)'
                    RS
             SU  PE
Automata
(a|b)*abb
Double circle
                    is acceptance
                    state
        b       a
            a       b
    b   0       1   a
            a                2
3
            b
baabb
b       a
            a       b
    b   0       1   a
            a           2
3
            b
b
        b       a
            a       b
    b   0       1   a
            a           2
3
            b
ba
        b        a
            a        b
    b   0        1   a
            a            2
3
            b
baa
        b         a
             a        b
    b   0         1   a
             a            2
3
             b
baabb
        b       a
            a       b
    b   0       1   a
            a           2
3
            b
aab
        b         a
             a        b
    b   0         1   a
             a            2
3
             b
Deterministic Finite
   Automaton
Only one path
Storage
class DFA
  attr_reader :table

  def initialize
    @table = Hash.new { |h, from| h[from] = {} }
    @table[0]['a'] = 1
    @table[0]['b'] = 0
    @table[1]['a'] = 1
    @table[1]['b'] = 2
    @table[2]['a'] = 1
    @table[2]['b'] = 3
    @table[3]['b'] = 0
    @table[3]['a'] = 2
  end
end
FSM Simulation
class Simulator
  def initialize(dfa)
    @dfa = dfa
  end

  def simulate(symbols)
    state = 0
    until symbols.empty?
      state = @dfa.move(state, symbols.shift)
    end
    state
  end
end
move function

class DFA
  ...

  def move(from, symbol)
    @table[from][symbol]
  end
end
irb> sim = Simulator.new(DFA.new)
=> #<Simulator:0x007f95929a82e0 ...>
irb> sim.simulate %w{ b a a b b }
=> 3
irb> sim.simulate %w{ a a b }
=> 2
Time: O(n)
 n = string.length
me gusta
Space: S + T
states.length + transitions.length
Nondeterministic
Finite Automaton
Has nil edges
Can't tell direction
Simulation of NFA
a|b

            a
    ε   2         3   ε
0   ε                 ε   6
            b
        4         5
nil-closure

             a
    ε    2       3    ε
0   ε                 ε   6
             b
         4       5
a

            a
    ε   2       3   ε
0   ε               ε   6
            b
        4       5
Storage
class NFA
  def initialize
    @table = Hash.new { |h, from|
      h[from] = Hash.new { |i,sym| i[sym] = [] }
    }

    @table[0][nil]   <<   2
    @table[0][nil]   <<   4
    @table[2]['a']   <<   3
    @table[4]['b']   <<   5
    @table[3][nil]   <<   6
    @table[5][nil]   <<   6
  end

  def nil_closure(states)
    states.map { |s| @table[s][nil] }.flatten
  end
end
FSM Simulation
class Simulator
  def initialize(nfa)
    @nfa = nfa
  end

  def simulate(symbols)
    states = @nfa.nil_closure([0])

    until symbols.empty?
      next_s = @nfa.move(states, symbols.shift)
      states = @nfa.nil_closure(next_s)
    end

    states
  end
end
Move function

class NFA
  ...

  def move(states, symbol)
    states.map { |s| @table[s][symbol] }.flatten
  end
end
irb> sim = Simulator.new(NFA.new)
=> #<Simulator:0x007faa188a5f88 ...>
irb> sim.simulate %w{ a }
=> [6]
irb> sim.simulate %w{ b }
=> [6]
irb> sim.simulate %w{ b b }
=> []
irb> sim.simulate %w{ c }
=> []
Time: O(r ⨉ x)
r = operators.length, x = string.length
Who cares about
    NFA?
NFA Construction
/articles(.:format)
            ○

        ○        ()

  /   articles    ○

             .        :format
cat nodes


    /
0       1
/articles


    /        articles
0        1              2
Optional

             ε
0   ε       ?????       ε   3
        1           2
ε
    /       articles
0       1              2   ε                             ε   6
                                   .       :format
                               3       4             5
Journey::NFA

parser = Journey::Parser.new
ast    = parser.parse '/articles(.:format)'
nfa    = Journey::NFA::Builder.new ast

tt = nfa.transition_table
puts tt.to_dot
Converting
NFA to DFA
Eliminate nil
 transitions
Collapse duplicate
      edges
/articles(.:format)

                                       ε
    /       articles
0       1              2   ε                             ε   6
                                   .       :format
                               3       4             5
/articles(.:format)

    /       articles       .       :format
0       1              2       3             4
CODES!

parser = Journey::Parser.new
ast    = parser.parse '/articles(.:format)'
nfa    = Journey::NFA::Builder.new ast

tt = nfa.transition_table.generalized_table
puts tt.to_dot
SHORTER CODES!

parser = Journey::Parser.new
ast    = parser.parse '/articles(.:format)'
dfa    = Journey::GTG::Builder.new ast

tt = dfa.transition_table
puts tt.to_dot
ANY NFA
converts to DFA
O(r ⨉ x) => O(x)
r = operations.length, x = string.length
me gusta
Converting Automata
    to Regexp
/articles(.:format)

    /       articles       .       :format
0       1              2       3             4
/articles(.:format)

    /articles       .       :format
0               2       3             4
/articles(.:format)

    /articles       .:format
0               2              4
/articles(.:format)

     /articles(.:format)?
0                           4
Generalized
Transition Graph
resource :users
/users(.:format)
/users/new(.:format)
/users/:id/edit(.:format)
/users/:id(.:format)
resource :users
                                (?-mix:[^./?]+)
                        .   3                     5
    /       users
0       1           2   /
                                     new              .        (?-mix:[^./?]+)
                            4                     6       8                      11
                                (?-mix:[^./?]+)

                                                      /             edit              .        (?-mix:[^./?]+)
                                                  7       9                      12       14                     15
                                                      .
                                                               (?-mix:[^./?]+)
                                                          10                     13
The Plan?
Combine All Routes
Produce DFA
Simulate in O(n) Time
Routing To The
   Future
JS Simulation
Table => JSON

parser = Journey::Parser.new
ast    = parser.parse '/articles(.:format)'
dfa    = Journey::GTG::Builder.new ast

tt = dfa.transition_table
puts tt.to_json
Table => SVG

parser = Journey::Parser.new
ast    = parser.parse '/articles(.:format)'
dfa    = Journey::GTG::Builder.new ast

tt = dfa.transition_table
puts tt.to_svg
JS Tokenizer


function tokenize(input, callback) {
  while(input.length > 0) {
    callback(input.match(/^[/.?]|[^/.?]+/)[0]);
    input = input.replace(/^[/.?]|[^/.?]+/, '');
  }
}
JS Simulator
tokenize(input, function(token) {
  var new_states = [];
  for(var key in states) {
    var state = states[key];

      if(string_states[state] && string_states[state][token]) {
        var new_state = string_states[state][token];
        highlight_edge(state, new_state);
        highlight_state(new_state);
        new_states.push(new_state);
      }
  }

  if(new_states.length == 0) {
    return;
  }
  states = new_states;
});
d3.js
Rails Console


irb> File.open('out.html', 'wb') { |f|
irb* f.write(
irb* Wot::Application.routes.router.visualizer
irb> )}
=> 69074
routes.rb marshalling
Test suggestions
Test coverage
Usage Heat Maps
AST



REGEXP         NFA



         DFA
ROFLSCALE!!!
DFA => YACC
DFA => RACC
DFA => Ragel
Open Questions
Is our GTG
deterministic?
/users/new|/users/:id

                                     new          4
    /       users       /       (?-mix:[^./?]+)
0       1           2       3
                                                  5
"new" =~ /new|[^./?]+/
Can we make it
deterministic?
L1 = {new}
L2 = {[^./?]+}
/users/new|/users/:id

                                   L1      4
     /       users       /       L2 - L1
0        1           2       3
                                           5
Is it worth our effort?
AST



REGEXP         NFA



         DFA
Is it worth our effort?
Thank You!!
<3<3<3<3<3

More Related Content

Similar to RubyConf Argentina 2011

Kernel Recipes 2019 - GNU poke, an extensible editor for structured binary data
Kernel Recipes 2019 - GNU poke, an extensible editor for structured binary dataKernel Recipes 2019 - GNU poke, an extensible editor for structured binary data
Kernel Recipes 2019 - GNU poke, an extensible editor for structured binary data
Anne Nicolas
 
4-Regular expression to Deterministic Finite Automata (Direct method)-05-05-2...
4-Regular expression to Deterministic Finite Automata (Direct method)-05-05-2...4-Regular expression to Deterministic Finite Automata (Direct method)-05-05-2...
4-Regular expression to Deterministic Finite Automata (Direct method)-05-05-2...
venkatapranaykumarGa
 
December10
December10December10
December10
khyps13
 
MongoDB: Replication,Sharding,MapReduce
MongoDB: Replication,Sharding,MapReduceMongoDB: Replication,Sharding,MapReduce
MongoDB: Replication,Sharding,MapReduce
Takahiro Inoue
 

Similar to RubyConf Argentina 2011 (20)

JSDC 2014 - functional java script, why or why not
JSDC 2014 - functional java script, why or why notJSDC 2014 - functional java script, why or why not
JSDC 2014 - functional java script, why or why not
 
Kernel Recipes 2019 - GNU poke, an extensible editor for structured binary data
Kernel Recipes 2019 - GNU poke, an extensible editor for structured binary dataKernel Recipes 2019 - GNU poke, an extensible editor for structured binary data
Kernel Recipes 2019 - GNU poke, an extensible editor for structured binary data
 
Ruby - Uma Introdução
Ruby - Uma IntroduçãoRuby - Uma Introdução
Ruby - Uma Introdução
 
Hacking parse.y (RubyConf 2009)
Hacking parse.y (RubyConf 2009)Hacking parse.y (RubyConf 2009)
Hacking parse.y (RubyConf 2009)
 
Python Fundamentals - Basic
Python Fundamentals - BasicPython Fundamentals - Basic
Python Fundamentals - Basic
 
4-Regular expression to Deterministic Finite Automata (Direct method)-05-05-2...
4-Regular expression to Deterministic Finite Automata (Direct method)-05-05-2...4-Regular expression to Deterministic Finite Automata (Direct method)-05-05-2...
4-Regular expression to Deterministic Finite Automata (Direct method)-05-05-2...
 
Estimating ecosystem functional features from intra-specific trait data
Estimating ecosystem functional features from intra-specific trait dataEstimating ecosystem functional features from intra-specific trait data
Estimating ecosystem functional features from intra-specific trait data
 
Instaduction to instaparse
Instaduction to instaparseInstaduction to instaparse
Instaduction to instaparse
 
[Ultracode Munich #4] Demo on Animatron by Anton Kotenko
[Ultracode Munich #4] Demo on Animatron by Anton Kotenko[Ultracode Munich #4] Demo on Animatron by Anton Kotenko
[Ultracode Munich #4] Demo on Animatron by Anton Kotenko
 
Scilab vs matlab
Scilab vs matlabScilab vs matlab
Scilab vs matlab
 
Graph Algebra
Graph AlgebraGraph Algebra
Graph Algebra
 
December10
December10December10
December10
 
MongoDB: Replication,Sharding,MapReduce
MongoDB: Replication,Sharding,MapReduceMongoDB: Replication,Sharding,MapReduce
MongoDB: Replication,Sharding,MapReduce
 
T5 2017 database_searching_v_upload
T5 2017 database_searching_v_uploadT5 2017 database_searching_v_upload
T5 2017 database_searching_v_upload
 
ROOT-LOCUS METHOD, Determine the root loci on the real axis /the asymptotes o...
ROOT-LOCUS METHOD, Determine the root loci on the real axis /the asymptotes o...ROOT-LOCUS METHOD, Determine the root loci on the real axis /the asymptotes o...
ROOT-LOCUS METHOD, Determine the root loci on the real axis /the asymptotes o...
 
Python Cheat Sheet
Python Cheat SheetPython Cheat Sheet
Python Cheat Sheet
 
C cheat sheet for varsity (extreme edition)
C cheat sheet for varsity (extreme edition)C cheat sheet for varsity (extreme edition)
C cheat sheet for varsity (extreme edition)
 
Quadratic Functions graph
Quadratic Functions graphQuadratic Functions graph
Quadratic Functions graph
 
Problemas resueltos de funciones lineales ccesa007
Problemas resueltos de  funciones lineales ccesa007Problemas resueltos de  funciones lineales ccesa007
Problemas resueltos de funciones lineales ccesa007
 
Rabotna tetratka 5 odd
Rabotna tetratka 5 oddRabotna tetratka 5 odd
Rabotna tetratka 5 odd
 

More from Aaron Patterson (8)

Nordic Ruby 2011
Nordic Ruby 2011Nordic Ruby 2011
Nordic Ruby 2011
 
RailsConf 2011 Keynote
RailsConf 2011 KeynoteRailsConf 2011 Keynote
RailsConf 2011 Keynote
 
Behind the Curtain
Behind the CurtainBehind the Curtain
Behind the Curtain
 
RubyConf Brazil 2010
RubyConf Brazil 2010RubyConf Brazil 2010
RubyConf Brazil 2010
 
Hidden Gems of Ruby 1.9
Hidden Gems of Ruby 1.9Hidden Gems of Ruby 1.9
Hidden Gems of Ruby 1.9
 
Having Fun Programming!
Having Fun Programming!Having Fun Programming!
Having Fun Programming!
 
Ruby on Rails: Tasty Burgers
Ruby on Rails: Tasty BurgersRuby on Rails: Tasty Burgers
Ruby on Rails: Tasty Burgers
 
Worst. Ideas. Ever.
Worst. Ideas. Ever.Worst. Ideas. Ever.
Worst. Ideas. Ever.
 

Recently uploaded

Future Visions: Predictions to Guide and Time Tech Innovation, Peter Udo Diehl
Future Visions: Predictions to Guide and Time Tech Innovation, Peter Udo DiehlFuture Visions: Predictions to Guide and Time Tech Innovation, Peter Udo Diehl
Future Visions: Predictions to Guide and Time Tech Innovation, Peter Udo Diehl
Peter Udo Diehl
 

Recently uploaded (20)

The Value of Certifying Products for FDO _ Paul at FIDO Alliance.pdf
The Value of Certifying Products for FDO _ Paul at FIDO Alliance.pdfThe Value of Certifying Products for FDO _ Paul at FIDO Alliance.pdf
The Value of Certifying Products for FDO _ Paul at FIDO Alliance.pdf
 
The UX of Automation by AJ King, Senior UX Researcher, Ocado
The UX of Automation by AJ King, Senior UX Researcher, OcadoThe UX of Automation by AJ King, Senior UX Researcher, Ocado
The UX of Automation by AJ King, Senior UX Researcher, Ocado
 
Google I/O Extended 2024 Warsaw
Google I/O Extended 2024 WarsawGoogle I/O Extended 2024 Warsaw
Google I/O Extended 2024 Warsaw
 
AI presentation and introduction - Retrieval Augmented Generation RAG 101
AI presentation and introduction - Retrieval Augmented Generation RAG 101AI presentation and introduction - Retrieval Augmented Generation RAG 101
AI presentation and introduction - Retrieval Augmented Generation RAG 101
 
Measures in SQL (a talk at SF Distributed Systems meetup, 2024-05-22)
Measures in SQL (a talk at SF Distributed Systems meetup, 2024-05-22)Measures in SQL (a talk at SF Distributed Systems meetup, 2024-05-22)
Measures in SQL (a talk at SF Distributed Systems meetup, 2024-05-22)
 
Future Visions: Predictions to Guide and Time Tech Innovation, Peter Udo Diehl
Future Visions: Predictions to Guide and Time Tech Innovation, Peter Udo DiehlFuture Visions: Predictions to Guide and Time Tech Innovation, Peter Udo Diehl
Future Visions: Predictions to Guide and Time Tech Innovation, Peter Udo Diehl
 
Custom Approval Process: A New Perspective, Pavel Hrbacek & Anindya Halder
Custom Approval Process: A New Perspective, Pavel Hrbacek & Anindya HalderCustom Approval Process: A New Perspective, Pavel Hrbacek & Anindya Halder
Custom Approval Process: A New Perspective, Pavel Hrbacek & Anindya Halder
 
WSO2CONMay2024OpenSourceConferenceDebrief.pptx
WSO2CONMay2024OpenSourceConferenceDebrief.pptxWSO2CONMay2024OpenSourceConferenceDebrief.pptx
WSO2CONMay2024OpenSourceConferenceDebrief.pptx
 
Speed Wins: From Kafka to APIs in Minutes
Speed Wins: From Kafka to APIs in MinutesSpeed Wins: From Kafka to APIs in Minutes
Speed Wins: From Kafka to APIs in Minutes
 
Top 10 Symfony Development Companies 2024
Top 10 Symfony Development Companies 2024Top 10 Symfony Development Companies 2024
Top 10 Symfony Development Companies 2024
 
Strategic AI Integration in Engineering Teams
Strategic AI Integration in Engineering TeamsStrategic AI Integration in Engineering Teams
Strategic AI Integration in Engineering Teams
 
Powerful Start- the Key to Project Success, Barbara Laskowska
Powerful Start- the Key to Project Success, Barbara LaskowskaPowerful Start- the Key to Project Success, Barbara Laskowska
Powerful Start- the Key to Project Success, Barbara Laskowska
 
Introduction to FDO and How It works Applications _ Richard at FIDO Alliance.pdf
Introduction to FDO and How It works Applications _ Richard at FIDO Alliance.pdfIntroduction to FDO and How It works Applications _ Richard at FIDO Alliance.pdf
Introduction to FDO and How It works Applications _ Richard at FIDO Alliance.pdf
 
A Business-Centric Approach to Design System Strategy
A Business-Centric Approach to Design System StrategyA Business-Centric Approach to Design System Strategy
A Business-Centric Approach to Design System Strategy
 
Salesforce Adoption – Metrics, Methods, and Motivation, Antone Kom
Salesforce Adoption – Metrics, Methods, and Motivation, Antone KomSalesforce Adoption – Metrics, Methods, and Motivation, Antone Kom
Salesforce Adoption – Metrics, Methods, and Motivation, Antone Kom
 
Oauth 2.0 Introduction and Flows with MuleSoft
Oauth 2.0 Introduction and Flows with MuleSoftOauth 2.0 Introduction and Flows with MuleSoft
Oauth 2.0 Introduction and Flows with MuleSoft
 
Intro in Product Management - Коротко про професію продакт менеджера
Intro in Product Management - Коротко про професію продакт менеджераIntro in Product Management - Коротко про професію продакт менеджера
Intro in Product Management - Коротко про професію продакт менеджера
 
PLAI - Acceleration Program for Generative A.I. Startups
PLAI - Acceleration Program for Generative A.I. StartupsPLAI - Acceleration Program for Generative A.I. Startups
PLAI - Acceleration Program for Generative A.I. Startups
 
Optimizing NoSQL Performance Through Observability
Optimizing NoSQL Performance Through ObservabilityOptimizing NoSQL Performance Through Observability
Optimizing NoSQL Performance Through Observability
 
10 Differences between Sales Cloud and CPQ, Blanka Doktorová
10 Differences between Sales Cloud and CPQ, Blanka Doktorová10 Differences between Sales Cloud and CPQ, Blanka Doktorová
10 Differences between Sales Cloud and CPQ, Blanka Doktorová
 

RubyConf Argentina 2011