Skip to content

Commit

Permalink
Merge 83b50e4 into 580756a
Browse files Browse the repository at this point in the history
  • Loading branch information
benlangfeld committed Sep 30, 2013
2 parents 580756a + 83b50e4 commit 153ff34
Show file tree
Hide file tree
Showing 12 changed files with 531 additions and 3 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
# [develop](https://github.com/benlangfeld/ruby_speech)
* Feature: Allow generation of a boolean, date, digits, currency, number, phone or time grammar including from URIs
* Bugfix: Ensure that rule refs can be reused when inlining grammars

# [2.2.2](https://github.com/benlangfeld/ruby_speech/compare/v2.2.1...v2.2.2) - [2013-09-03](https://rubygems.org/gems/ruby_speech/versions/2.2.2)
* Bugfix: Fix an exception message to include object type
Expand Down
41 changes: 41 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,47 @@ which becomes
</grammar>
```

#### Built-in grammars

There are some grammars pre-defined which are available from the `RubySpeech::GRXML::Builtins` module like so.

```ruby
require 'ruby_speech'

RubySpeech::GRXML::Builtins.currency
```

which yields

```xml
<grammar xmlns="http://www.w3.org/2001/06/grammar" version="1.0" xml:lang="en-US" mode="dtmf" root="currency">
<rule id="currency" scope="public">
<item repeat="0-">
<ruleref uri="#digit"/>
</item>
<item>*</item>
<item repeat="2">
<ruleref uri="#digit"/>
</item>
</rule>
<rule id="digit">
<one-of>
<item>0</item>
<item>1</item>
<item>2</item>
<item>3</item>
<item>4</item>
<item>5</item>
<item>6</item>
<item>7</item>
<item>8</item>
<item>9</item>
</one-of>
</rule>
</grammar>
```
These grammars can be used just like any you would manually create, and there's nothing special about them except that they are already defined for you.

#### Grammar matching

It is possible to match some arbitrary input against a GRXML grammar, like so:
Expand Down
4 changes: 2 additions & 2 deletions ext/ruby_speech/ruby_speech.c
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ static int is_match_end(pcre *compiled_regex, const char *input)
search_input[input_size] = *search++;
result = pcre_exec(compiled_regex, NULL, search_input, input_size + 1, 0, 0,
ovector, sizeof(ovector) / sizeof(ovector[0]));
if (result > 0) return 0;
if (result > -1) return 0;
}
return 1;
}
Expand Down Expand Up @@ -82,7 +82,7 @@ static VALUE method_find_match(VALUE self, VALUE buffer)
}
return rb_funcall(self, rb_intern("match_for_buffer"), 1, buffer);
}
if (result == PCRE_ERROR_PARTIAL) {
if (result == PCRE_ERROR_PARTIAL || (int)strlen(input) == 0) {
VALUE PotentialMatch = rb_const_get(GRXML, rb_intern("PotentialMatch"));
return rb_class_new_instance(0, NULL, PotentialMatch);
}
Expand Down
24 changes: 24 additions & 0 deletions lib/ruby_speech/grxml.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ module GRXML
GRXML_NAMESPACE = 'http://www.w3.org/2001/06/grammar'

%w{
builtins
grammar
rule
item
Expand All @@ -29,5 +30,28 @@ def self.draw(attributes = {}, &block)
def self.import(other)
Element.import other
end

URI_REGEX = /builtin:(?<class>.*)\/(?<type>\w*)(\?)?(?<query>(\w*=\w*;?)*)?/.freeze

#
# Fetch a builtin grammar by URI
#
# @param [String] uri The builtin grammar URI of the form "builtin:dtmf/type?param=value"
#
# @return [RubySpeech::GRXML::Grammar] a grammar from the builtin set
#
def self.from_uri(uri)
match = uri.match(URI_REGEX)
raise ArgumentError, "Only builtin grammars are supported" unless match
raise ArgumentError, "Only DTMF builtins are supported" unless match[:class] == 'dtmf'
type = match[:type]
query = {}
match[:query].split(';').each do |s|
key, value = s.split('=')
query[key] = value
end
raise ArgumentError, "#{type} is an invalid builtin grammar" unless Builtins.respond_to?(type)
Builtins.send type, query
end
end # GRXML
end # RubySpeech
174 changes: 174 additions & 0 deletions lib/ruby_speech/grxml/builtins.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
module RubySpeech::GRXML::Builtins
#
# Create a grammar for interpreting a boolean response, where 1 is true and two is false.
#
# @param [Hash] options Options to parameterize the grammar
# @option options [#to_s] :y The positive/truthy/affirmative digit
# @option options [#to_s] :n The negative/falsy digit
#
# @return [RubySpeech::GRXML::Grammar] a grammar for interpreting a boolean response.
#
# @raise [ArgumentError] if the :y and :n options are the same
#
def self.boolean(options = {})
truthy_digit = (options[:y] || options['y'] || '1').to_s
falsy_digit = (options[:n] || options['n'] || '2').to_s

raise ArgumentError, "Yes and no values cannot be the same" if truthy_digit == falsy_digit

RubySpeech::GRXML.draw mode: :dtmf, root: 'boolean' do
rule id: 'boolean', scope: 'public' do
one_of do
item do
tag { 'true' }
truthy_digit
end
item do
tag { 'false' }
falsy_digit
end
end
end
end
end

#
# Create a grammar for interpreting a date.
#
# @return [RubySpeech::GRXML::Grammar] a grammar for interpreting a date in the format yyyymmdd
#
def self.date(options = nil)
RubySpeech::GRXML.draw mode: :dtmf, root: 'date' do
rule id: 'date', scope: 'public' do
item repeat: '8' do
one_of do
0.upto(9) { |d| item { d.to_s } }
end
end
end
end
end

#
# Create a grammar for interpreting a string of digits.
#
# @param [Hash] options Options to parameterize the grammar
# @option options [#to_i] :minlength Minimum length for the string of digits.
# @option options [#to_i] :maxlength Maximum length for the string of digits.
# @option options [#to_i] :length Absolute length for the string of digits.
#
# @return [RubySpeech::GRXML::Grammar] a grammar for interpreting a boolean response.
#
# @raise [ArgumentError] if any of the length attributes logically conflict
#
def self.digits(options = {})
raise ArgumentError, "Cannot specify both absolute length and a length range" if options[:length] && (options[:minlength] || options[:maxlength])

minlength = options[:minlength] || options['minlength'] || 0
maxlength = options[:maxlength] || options['maxlength']
length = options[:length] || options['length']

repeat = length ? length : "#{minlength}-#{maxlength}"

RubySpeech::GRXML.draw mode: :dtmf, root: 'digits' do
rule id: 'digits', scope: 'public' do
item repeat: repeat do
one_of do
0.upto(9) { |d| item { d.to_s } }
end
end
end
end
end

#
# Create a grammar for interpreting a monetary value. Uses '*' as the decimal point.
# Matches any number of digits, optionally followed by a '*' and up to two more digits.
#
# @return [RubySpeech::GRXML::Grammar] a grammar for interpreting a monetary value.
#
def self.currency(options = nil)
RubySpeech::GRXML.draw mode: :dtmf, root: 'currency' do
rule id: 'currency', scope: 'public' do
item repeat: '0-' do
ruleref uri: '#digit'
end
item repeat: '0-1' do
item { '*' }
item repeat: '0-2' do
ruleref uri: '#digit'
end
end
end

rule id: 'digit' do
one_of do
0.upto(9) { |d| item { d.to_s } }
end
end
end
end

#
# Create a grammar for interpreting a numeric value. Uses '*' as the decimal point.
# Matches any number of digits, optionally followed by a '*' and any number more digits.
#
# @return [RubySpeech::GRXML::Grammar] a grammar for interpreting a numeric value.
#
def self.number(options = nil)
RubySpeech::GRXML.draw mode: :dtmf, root: 'number' do
rule id: 'number', scope: 'public' do
item repeat: '0-' do
ruleref uri: '#digit'
end
item repeat: '0-1' do
item { '*' }
item repeat: '0-' do
ruleref uri: '#digit'
end
end
end

rule id: 'digit' do
one_of do
0.upto(9) { |d| item { d.to_s } }
end
end
end
end

#
# Create a grammar for interpreting a phone number. Uses '*' to represent 'x' for a number with an extension.
#
# @return [RubySpeech::GRXML::Grammar] a grammar for interpreting a phone number.
#
def self.phone(options = nil)
RubySpeech::GRXML.draw mode: :dtmf, root: 'number' do
rule id: 'number', scope: 'public' do
item repeat: '1-' do
one_of do
0.upto(9) { |d| item { d.to_s } }
item { '*' }
end
end
end
end
end

#
# Create a grammar for interpreting a time.
#
# @return [RubySpeech::GRXML::Grammar] a grammar for interpreting a time.
#
def self.time(options = nil)
RubySpeech::GRXML.draw mode: :dtmf, root: 'time' do
rule id: 'time', scope: 'public' do
item repeat: '1-4' do
one_of do
0.upto(9) { |d| item { d.to_s } }
end
end
end
end
end
end
2 changes: 1 addition & 1 deletion lib/ruby_speech/grxml/grammar.rb
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ def inline
def inline!
xpath("//ns:ruleref", :ns => GRXML_NAMESPACE).each do |ref|
rule = rule_with_id ref[:uri].sub(/^#/, '')
ref.swap rule.children
ref.swap rule.dup.children
end

query = "./ns:rule[@id!='#{root}']"
Expand Down

0 comments on commit 153ff34

Please sign in to comment.