From 474c326cf871e40f02fd576ba57bbb71c136d8ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Carlos=20Nieto?= <jose.carlos@menteslibres.net> Date: Tue, 18 Nov 2014 13:38:23 -0600 Subject: [PATCH] mongo: Adding support for ConnectURL. --- mongo/connection.go | 122 ++++++++++++++++++++++++++++ mongo/connection_test.go | 136 +++++++++++++++++++++++++++++++ mongo/database.go | 170 +++++++++++++++++++-------------------- mongo/database_test.go | 161 +++++++++++++++++++++++++++++------- 4 files changed, 470 insertions(+), 119 deletions(-) create mode 100644 mongo/connection.go create mode 100644 mongo/connection_test.go diff --git a/mongo/connection.go b/mongo/connection.go new file mode 100644 index 00000000..e0055ec0 --- /dev/null +++ b/mongo/connection.go @@ -0,0 +1,122 @@ +// Copyright (c) 2012-2014 José Carlos Nieto, https://menteslibres.net/xiam +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +package mongo + +import ( + "fmt" + "net/url" + "strings" + "upper.io/db" +) + +const connectionScheme = `mongodb` + +type ConnectionURL struct { + User string + Password string + Address db.Address + Database string + Options map[string]string +} + +func (c ConnectionURL) String() (s string) { + + if c.Database == "" { + return "" + } + + vv := url.Values{} + + // Do we have any options? + if c.Options == nil { + c.Options = map[string]string{} + } + + // Converting options into URL values. + for k, v := range c.Options { + vv.Set(k, v) + } + + // Has user? + var userInfo *url.Userinfo + + if c.User != "" { + if c.Password == "" { + userInfo = url.User(c.User) + } else { + userInfo = url.UserPassword(c.User, c.Password) + } + } + + // Building URL. + u := url.URL{ + Scheme: connectionScheme, + Path: c.Database, + User: userInfo, + RawQuery: vv.Encode(), + } + + if c.Address != nil { + u.Host = c.Address.String() + } + + return u.String() +} + +func ParseURL(s string) (conn ConnectionURL, err error) { + var u *url.URL + + if strings.HasPrefix(s, connectionScheme+"://") == false { + return conn, fmt.Errorf(`Expecting mongodb:// connection scheme.`) + } + + if u, err = url.Parse(s); err != nil { + return conn, err + } + + // Parsing host. + conn.Address = db.ParseAddress(u.Host) + + // Deleting / from start of the string. + conn.Database = strings.Trim(u.Path, "/") + + // Adding user / password. + if u.User != nil { + conn.User = u.User.Username() + conn.Password, _ = u.User.Password() + } + + // Adding options. + conn.Options = map[string]string{} + + var vv url.Values + + if vv, err = url.ParseQuery(u.RawQuery); err != nil { + return conn, err + } + + for k := range vv { + conn.Options[k] = vv.Get(k) + } + + return conn, err +} diff --git a/mongo/connection_test.go b/mongo/connection_test.go new file mode 100644 index 00000000..20cf1f96 --- /dev/null +++ b/mongo/connection_test.go @@ -0,0 +1,136 @@ +// Copyright (c) 2012-2014 José Carlos Nieto, https://menteslibres.net/xiam +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +package mongo + +import ( + "testing" + "upper.io/db" +) + +func TestConnectionURL(t *testing.T) { + + c := ConnectionURL{} + + // Default connection string is only the protocol. + if c.String() != "" { + t.Fatal(`Expecting default connectiong string to be empty, got:`, c.String()) + } + + // Adding a database name. + c.Database = "myfilename" + + if c.String() != "mongodb://myfilename" { + t.Fatal(`Test failed, got:`, c.String()) + } + + // Adding an option. + c.Options = map[string]string{ + "cache": "foobar", + "mode": "ro", + } + + // Adding username and password + c.User = "user" + c.Password = "pass" + + // Setting host. + c.Address = db.Host("localhost") + + if c.String() != "mongodb://user:pass@localhost/myfilename?cache=foobar&mode=ro" { + t.Fatal(`Test failed, got:`, c.String()) + } + + // Setting host and port. + c.Address = db.HostPort("localhost", 27017) + + if c.String() != "mongodb://user:pass@localhost:27017/myfilename?cache=foobar&mode=ro" { + t.Fatal(`Test failed, got:`, c.String()) + } + + // Setting cluster. + c.Address = db.Cluster(db.Host("localhost"), db.Host("1.2.3.4"), db.HostPort("example.org", 1234)) + + if c.String() != "mongodb://user:pass@localhost,1.2.3.4,example.org:1234/myfilename?cache=foobar&mode=ro" { + t.Fatal(`Test failed, got:`, c.String()) + } + + // Setting another database. + c.Database = "another_database" + + if c.String() != "mongodb://user:pass@localhost,1.2.3.4,example.org:1234/another_database?cache=foobar&mode=ro" { + t.Fatal(`Test failed, got:`, c.String()) + } + +} + +func TestParseConnectionURL(t *testing.T) { + var u ConnectionURL + var s string + var err error + + s = "mongodb:///mydatabase" + + if u, err = ParseURL(s); err != nil { + t.Fatal(err) + } + + if u.Database != "mydatabase" { + t.Fatal("Failed to parse database.") + } + + s = "mongodb://user:pass@localhost,1.2.3.4,example.org:1234/another_database?cache=foobar&mode=ro" + + if u, err = ParseURL(s); err != nil { + t.Fatal(err) + } + + if u.Database != "another_database" { + t.Fatal("Failed to get database.") + } + + if u.Options["cache"] != "foobar" { + t.Fatal("Expecting option.") + } + + if u.Options["mode"] != "ro" { + t.Fatal("Expecting option.") + } + + if u.User != "user" { + t.Fatal("Expecting user.") + } + + if u.Password != "pass" { + t.Fatal("Expecting password.") + } + + if u.Address.String() != "localhost,1.2.3.4,example.org:1234" { + t.Fatal("Expecting host.") + } + + s = "http://example.org" + + if _, err = ParseURL(s); err == nil { + t.Fatal("Expecting error.") + } + +} diff --git a/mongo/database.go b/mongo/database.go index 8a54337c..ede0c73b 100644 --- a/mongo/database.go +++ b/mongo/database.go @@ -1,32 +1,29 @@ -/* - Copyright (c) 2012-2014 José Carlos Nieto, https://menteslibres.net/xiam - - Permission is hereby granted, free of charge, to any person obtaining - a copy of this software and associated documentation files (the - "Software"), to deal in the Software without restriction, including - without limitation the rights to use, copy, modify, merge, publish, - distribute, sublicense, and/or sell copies of the Software, and to - permit persons to whom the Software is furnished to do so, subject to - the following conditions: - - The above copyright notice and this permission notice shall be - included in all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF - MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE - LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION - OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -*/ +// Copyright (c) 2012-2014 José Carlos Nieto, https://menteslibres.net/xiam +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. package mongo import ( "fmt" "log" - "net/url" "os" "strings" "time" @@ -41,7 +38,7 @@ var connTimeout = time.Second * 5 type Source struct { name string - config db.Settings + connURL db.ConnectionURL session *mgo.Session database *mgo.Database } @@ -62,119 +59,114 @@ func debugLogQuery(c *chunks) { } // Returns the string name of the database. -func (self *Source) Name() string { - return self.name +func (s *Source) Name() string { + return s.name } // Stores database settings. -func (self *Source) Setup(config db.Settings) error { - self.config = config - return self.Open() +func (s *Source) Setup(connURL db.ConnectionURL) error { + s.connURL = connURL + return s.Open() } -func (self *Source) Clone() (db.Database, error) { +func (s *Source) Clone() (db.Database, error) { clone := &Source{ - name: self.name, - config: self.config, - session: self.session.Copy(), - database: self.database, + name: s.name, + connURL: s.connURL, + session: s.session.Copy(), + database: s.database, } return clone, nil } -func (self *Source) Transaction() (db.Tx, error) { +func (s *Source) Transaction() (db.Tx, error) { return nil, db.ErrUnsupported } -func (self *Source) Ping() error { - return self.session.Ping() +func (s *Source) Ping() error { + return s.session.Ping() } // Returns the underlying *mgo.Session instance. -func (self *Source) Driver() interface{} { - return self.session +func (s *Source) Driver() interface{} { + return s.session } // Attempts to connect to a database using the stored settings. -func (self *Source) Open() error { +func (s *Source) Open() error { var err error - connURL := &url.URL{Scheme: `mongodb`} - - if self.config.Port == 0 { - self.config.Port = 27017 - } - - if self.config.Host == "" { - self.config.Host = `127.0.0.1` - } - - connURL.Host = fmt.Sprintf(`%s:%d`, self.config.Host, self.config.Port) - - if self.config.User != "" { - connURL.User = url.UserPassword(self.config.User, self.config.Password) - } + // Before db.ConnectionURL we used a unified db.Settings struct. This + // condition checks for that type and provides backwards compatibility. + if settings, ok := s.connURL.(db.Settings); ok { + var sAddr string + + if settings.Host != "" { + if settings.Port > 0 { + sAddr = fmt.Sprintf("%s:%d", settings.Host, settings.Port) + } else { + sAddr = settings.Host + } + } else { + sAddr = settings.Socket + } - if self.config.Database != "" { - connURL.Path = "/" + self.config.Database - } + conn := ConnectionURL{ + User: settings.User, + Password: settings.Password, + Address: db.ParseAddress(sAddr), + Database: settings.Database, + } - if self.config.Database == "" { - return db.ErrMissingDatabaseName + // Replace original s.connURL + s.connURL = conn } - if self.session, err = mgo.DialWithTimeout(connURL.String(), connTimeout); err != nil { + if s.session, err = mgo.DialWithTimeout(s.connURL.String(), connTimeout); err != nil { return err } - self.Use(self.config.Database) + s.database = s.session.DB("") return nil } // Closes the current database session. -func (self *Source) Close() error { - if self.session != nil { - self.session.Close() +func (s *Source) Close() error { + if s.session != nil { + s.session.Close() } return nil } // Changes the active database. -func (self *Source) Use(database string) error { - self.config.Database = database - self.name = database - self.database = self.session.DB(self.config.Database) - return nil -} +func (s *Source) Use(database string) (err error) { + var conn ConnectionURL -// Starts a transaction block. -func (self *Source) Begin() error { - // TODO: - // MongoDB does not supports something like BEGIN and END statements. - return nil -} + if conn, err = ParseURL(s.connURL.String()); err != nil { + return err + } -// Ends a transaction block. -func (self *Source) End() error { - // TODO: - // MongoDB does not supports something like BEGIN and END statements. - return nil + conn.Database = database + + s.connURL = conn + + return s.Open() } // Drops the currently active database. -func (self *Source) Drop() error { - err := self.database.DropDatabase() +func (s *Source) Drop() error { + err := s.database.DropDatabase() return err } // Returns a slice of non-system collection names within the active // database. -func (self *Source) Collections() (cols []string, err error) { +func (s *Source) Collections() (cols []string, err error) { var rawcols []string var col string - if rawcols, err = self.database.CollectionNames(); err != nil { + if rawcols, err = s.database.CollectionNames(); err != nil { return nil, err } @@ -190,7 +182,7 @@ func (self *Source) Collections() (cols []string, err error) { } // Returns a collection instance by name. -func (self *Source) Collection(names ...string) (db.Collection, error) { +func (s *Source) Collection(names ...string) (db.Collection, error) { var err error if len(names) > 1 { @@ -200,8 +192,8 @@ func (self *Source) Collection(names ...string) (db.Collection, error) { name := names[0] col := &Collection{} - col.parent = self - col.collection = self.database.C(name) + col.parent = s + col.collection = s.database.C(name) if col.Exists() == false { err = db.ErrCollectionDoesNotExist diff --git a/mongo/database_test.go b/mongo/database_test.go index 2c22eb0c..5b6e9b5b 100644 --- a/mongo/database_test.go +++ b/mongo/database_test.go @@ -1,29 +1,29 @@ -/* - Copyright (c) 2012-2014 José Carlos Nieto, https://menteslibres.net/xiam - - Permission is hereby granted, free of charge, to any person obtaining - a copy of this software and associated documentation files (the - "Software"), to deal in the Software without restriction, including - without limitation the rights to use, copy, modify, merge, publish, - distribute, sublicense, and/or sell copies of the Software, and to - permit persons to whom the Software is furnished to do so, subject to - the following conditions: - - The above copyright notice and this permission notice shall be - included in all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF - MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE - LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION - OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -*/ +// Copyright (c) 2012-2014 José Carlos Nieto, https://menteslibres.net/xiam +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + // Tests for the mongodb adapter. package mongo import ( + "flag" "math/rand" "os" "reflect" @@ -39,18 +39,20 @@ import ( // Wrapper settings. const ( - host = "testserver.local" - dbname = "upperio_tests" + database = "upperio_tests" + username = "upperio" + password = "upperio" ) // Global settings for tests. -var settings = db.Settings{ - Host: host, - Database: dbname, - User: "upperio", - Password: "upperio", +var settings = ConnectionURL{ + Database: database, + User: username, + Password: password, } +var host = flag.String("host", "testserver.local", "Testing server address.") + // Structure for testing conversions and datatypes. type testValuesStruct struct { Uint uint `bson:"_uint"` @@ -93,6 +95,9 @@ func init() { &t, time.Second * time.Duration(7331), } + + flag.Parse() + settings.Address = db.ParseAddress(*host) } // Enabling outputting some information to stdout, useful for development. @@ -111,6 +116,102 @@ func TestOpenFailed(t *testing.T) { } */ +// Attempts to open an empty datasource. +func TestOpenWithWrongData(t *testing.T) { + var err error + var rightSettings, wrongSettings db.Settings + + // Attempt to open with safe settings. + rightSettings = db.Settings{ + Database: database, + Host: *host, + User: username, + Password: password, + } + + // Attempt to open an empty database. + if _, err = db.Open(Adapter, rightSettings); err != nil { + // Must fail. + t.Fatal(err) + } + + // Attempt to open with wrong password. + wrongSettings = db.Settings{ + Database: database, + Host: *host, + User: username, + Password: "fail", + } + + if _, err = db.Open(Adapter, wrongSettings); err == nil { + t.Fatalf("Expecting an error.") + } + + // Attempt to open with wrong database. + wrongSettings = db.Settings{ + Database: "fail", + Host: *host, + User: username, + Password: password, + } + + if _, err = db.Open(Adapter, wrongSettings); err == nil { + t.Fatalf("Expecting an error.") + } + + // Attempt to open with wrong username. + wrongSettings = db.Settings{ + Database: database, + Host: *host, + User: "fail", + Password: password, + } + + if _, err = db.Open(Adapter, wrongSettings); err == nil { + t.Fatalf("Expecting an error.") + } +} + +// Old settings must be compatible. +func TestOldSettings(t *testing.T) { + var err error + var sess db.Database + + oldSettings := db.Settings{ + Database: database, + User: username, + Password: password, + Host: settings.Address.String(), + } + + // Opening database. + if sess, err = db.Open(Adapter, oldSettings); err != nil { + t.Fatal(err) + } + + // Closing database. + sess.Close() +} + +// Test USE +func TestUse(t *testing.T) { + var err error + var sess db.Database + + // Opening database, no error expected. + if sess, err = db.Open(Adapter, settings); err != nil { + t.Fatal(err) + } + + // Connecting to another database, error expected. + if err = sess.Use("."); err == nil { + t.Fatal("This is not a database") + } + + // Closing connection. + sess.Close() +} + // Truncates all collections. func TestTruncate(t *testing.T) { @@ -768,7 +869,7 @@ func BenchmarkAppendRaw(b *testing.B) { driver := sess.Driver().(*mgo.Session) - mgodb := driver.DB(dbname) + mgodb := driver.DB(database) col := mgodb.C("artist") b.ResetTimer() -- GitLab