diff --git a/.all-contributorsrc b/.all-contributorsrc deleted file mode 100644 index 3f3f3ec9..00000000 --- a/.all-contributorsrc +++ /dev/null @@ -1,2752 +0,0 @@ -{ - "files": [ - "AUTHORS.md" - ], - "imageSize": 100, - "commit": false, - "contributors": [ - { - "login": "carols10cents", - "name": "Carol (Nichols || Goulding)", - "avatar_url": "https://avatars2.githubusercontent.com/u/193874?v=4", - "profile": "http://carol-nichols.com", - "contributions": [ - "code", - "content" - ] - }, - { - "login": "QuietMisdreavus", - "name": "QuietMisdreavus", - "avatar_url": "https://avatars2.githubusercontent.com/u/5217170?v=4", - "profile": "https://twitter.com/QuietMisdreavus", - "contributions": [ - "code", - "content" - ] - }, - { - "login": "robertlugg", - "name": "Robert M Lugg", - "avatar_url": "https://avatars0.githubusercontent.com/u/6054540?v=4", - "profile": "https://github.com/robertlugg", - "contributions": [ - "content" - ] - }, - { - "login": "hynek", - "name": "Hynek Schlawack", - "avatar_url": "https://avatars3.githubusercontent.com/u/41240?v=4", - "profile": "https://hynek.me/about/", - "contributions": [ - "code" - ] - }, - { - "login": "spacekookie", - "name": "Katharina Fey", - "avatar_url": "https://avatars0.githubusercontent.com/u/7669898?v=4", - "profile": "https://spacekookie.de", - "contributions": [ - "code" - ] - }, - { - "login": "lukabavdaz", - "name": "lukabavdaz", - "avatar_url": "https://avatars0.githubusercontent.com/u/9624558?v=4", - "profile": "https://github.com/lukabavdaz", - "contributions": [ - "code", - "content" - ] - }, - { - "login": "evestera", - "name": "Erik Vesteraas", - "avatar_url": "https://avatars2.githubusercontent.com/u/4187449?v=4", - "profile": "http://vestera.as", - "contributions": [ - "code" - ] - }, - { - "login": "Delet0r", - "name": "delet0r", - "avatar_url": "https://avatars1.githubusercontent.com/u/23195618?v=4", - "profile": "https://github.com/Delet0r", - "contributions": [ - "code" - ] - }, - { - "login": "shaunbennett", - "name": "Shaun Bennett", - "avatar_url": "https://avatars1.githubusercontent.com/u/10522375?v=4", - "profile": "http://phinary.ca", - "contributions": [ - "code" - ] - }, - { - "login": "abagshaw", - "name": "Andrew Bagshaw", - "avatar_url": "https://avatars2.githubusercontent.com/u/8594541?v=4", - "profile": "https://github.com/abagshaw", - "contributions": [ - "code" - ] - }, - { - "login": "kisom", - "name": "Kyle Isom", - "avatar_url": "https://avatars2.githubusercontent.com/u/175578?v=4", - "profile": "https://ai6ua.net/", - "contributions": [ - "code" - ] - }, - { - "login": "ColinPitrat", - "name": "Colin Pitrat", - "avatar_url": "https://avatars3.githubusercontent.com/u/1541863?v=4", - "profile": "https://github.com/ColinPitrat", - "contributions": [ - "code" - ] - }, - { - "login": "zacanger", - "name": "Zac Anger", - "avatar_url": "https://avatars3.githubusercontent.com/u/12520493?v=4", - "profile": "https://zacanger.com", - "contributions": [ - "code" - ] - }, - { - "login": "mgeier", - "name": "Matthias Geier", - "avatar_url": "https://avatars1.githubusercontent.com/u/705404?v=4", - "profile": "https://github.com/mgeier", - "contributions": [ - "code" - ] - }, - { - "login": "cjpearce", - "name": "Chris Pearce", - "avatar_url": "https://avatars1.githubusercontent.com/u/3453268?v=4", - "profile": "https://github.com/cjpearce", - "contributions": [ - "code" - ] - }, - { - "login": "yvan-sraka", - "name": "Yvan Sraka", - "avatar_url": "https://avatars2.githubusercontent.com/u/705213?v=4", - "profile": "https://yvan-sraka.github.io", - "contributions": [ - "code" - ] - }, - { - "login": "dendi239", - "name": "Denys Smirnov", - "avatar_url": "https://avatars3.githubusercontent.com/u/16478650?v=4", - "profile": "https://github.com/dendi239", - "contributions": [ - "code" - ] - }, - { - "login": "eddyp", - "name": "eddyp", - "avatar_url": "https://avatars2.githubusercontent.com/u/123772?v=4", - "profile": "https://github.com/eddyp", - "contributions": [ - "code" - ] - }, - { - "login": "briankung", - "name": "Brian Kung", - "avatar_url": "https://avatars1.githubusercontent.com/u/2836167?v=4", - "profile": "http://about.me/BrianKung", - "contributions": [ - "code", - "content" - ] - }, - { - "login": "miller-time", - "name": "Russell", - "avatar_url": "https://avatars3.githubusercontent.com/u/281039?v=4", - "profile": "https://rcousineau.gitlab.io", - "contributions": [ - "code" - ] - }, - { - "login": "danwilhelm", - "name": "Dan Wilhelm", - "avatar_url": "https://avatars3.githubusercontent.com/u/6137185?v=4", - "profile": "http://danwilhelm.com", - "contributions": [ - "doc" - ] - }, - { - "login": "Jesse-Cameron", - "name": "Jesse", - "avatar_url": "https://avatars3.githubusercontent.com/u/3723654?v=4", - "profile": "https://github.com/Jesse-Cameron", - "contributions": [ - "code", - "content" - ] - }, - { - "login": "MrFroop", - "name": "Fredrik Jambrén", - "avatar_url": "https://avatars3.githubusercontent.com/u/196700?v=4", - "profile": "https://github.com/MrFroop", - "contributions": [ - "code" - ] - }, - { - "login": "petemcfarlane", - "name": "Pete McFarlane", - "avatar_url": "https://avatars3.githubusercontent.com/u/3472717?v=4", - "profile": "https://github.com/petemcfarlane", - "contributions": [ - "content" - ] - }, - { - "login": "nkanderson", - "name": "nkanderson", - "avatar_url": "https://avatars0.githubusercontent.com/u/4128825?v=4", - "profile": "https://github.com/nkanderson", - "contributions": [ - "code", - "content" - ] - }, - { - "login": "ajaxm", - "name": "Ajax M", - "avatar_url": "https://avatars0.githubusercontent.com/u/13360138?v=4", - "profile": "https://github.com/ajaxm", - "contributions": [ - "doc" - ] - }, - { - "login": "Dylnuge", - "name": "Dylan Nugent", - "avatar_url": "https://avatars2.githubusercontent.com/u/118624?v=4", - "profile": "https://dylnuge.com", - "contributions": [ - "content" - ] - }, - { - "login": "vyaslav", - "name": "vyaslav", - "avatar_url": "https://avatars0.githubusercontent.com/u/1385427?v=4", - "profile": "https://github.com/vyaslav", - "contributions": [ - "code", - "content" - ] - }, - { - "login": "gdoenlen", - "name": "George", - "avatar_url": "https://avatars1.githubusercontent.com/u/17297466?v=4", - "profile": "https://join.sfxd.org", - "contributions": [ - "code" - ] - }, - { - "login": "nyxtom", - "name": "Thomas Holloway", - "avatar_url": "https://avatars2.githubusercontent.com/u/222763?v=4", - "profile": "https://github.com/nyxtom", - "contributions": [ - "code", - "content" - ] - }, - { - "login": "workingjubilee", - "name": "Jubilee", - "avatar_url": "https://avatars1.githubusercontent.com/u/46493976?v=4", - "profile": "https://github.com/workingjubilee", - "contributions": [ - "code" - ] - }, - { - "login": "WofWca", - "name": "WofWca", - "avatar_url": "https://avatars1.githubusercontent.com/u/39462442?v=4", - "profile": "https://github.com/WofWca", - "contributions": [ - "code" - ] - }, - { - "login": "jrvidal", - "name": "Roberto Vidal", - "avatar_url": "https://avatars0.githubusercontent.com/u/1636604?v=4", - "profile": "https://github.com/jrvidal", - "contributions": [ - "code", - "doc", - "ideas", - "maintenance" - ] - }, - { - "login": "jensim", - "name": "Jens", - "avatar_url": "https://avatars0.githubusercontent.com/u/3663856?v=4", - "profile": "https://github.com/jensim", - "contributions": [ - "doc" - ] - }, - { - "login": "rahatarmanahmed", - "name": "Rahat Ahmed", - "avatar_url": "https://avatars3.githubusercontent.com/u/3174006?v=4", - "profile": "http://rahatah.me/d", - "contributions": [ - "doc" - ] - }, - { - "login": "AbdouSeck", - "name": "Abdou Seck", - "avatar_url": "https://avatars2.githubusercontent.com/u/6490055?v=4", - "profile": "https://github.com/AbdouSeck", - "contributions": [ - "code", - "content", - "review" - ] - }, - { - "login": "codehearts", - "name": "Katie", - "avatar_url": "https://avatars0.githubusercontent.com/u/2885412?v=4", - "profile": "https://codehearts.com", - "contributions": [ - "code" - ] - }, - { - "login": "Socratides", - "name": "Socrates", - "avatar_url": "https://avatars3.githubusercontent.com/u/27732983?v=4", - "profile": "https://github.com/Socratides", - "contributions": [ - "doc" - ] - }, - { - "login": "gnodarse", - "name": "gnodarse", - "avatar_url": "https://avatars3.githubusercontent.com/u/46761795?v=4", - "profile": "https://github.com/gnodarse", - "contributions": [ - "content" - ] - }, - { - "login": "harrisonmetz", - "name": "Harrison Metzger", - "avatar_url": "https://avatars1.githubusercontent.com/u/7883408?v=4", - "profile": "https://github.com/harrisonmetz", - "contributions": [ - "code" - ] - }, - { - "login": "TorbenJ", - "name": "Torben Jonas", - "avatar_url": "https://avatars2.githubusercontent.com/u/9077102?v=4", - "profile": "https://github.com/TorbenJ", - "contributions": [ - "code", - "content" - ] - }, - { - "login": "pbx", - "name": "Paul Bissex", - "avatar_url": "https://avatars0.githubusercontent.com/u/641?v=4", - "profile": "http://paulbissex.com/", - "contributions": [ - "doc" - ] - }, - { - "login": "sjmann", - "name": "Steven Mann", - "avatar_url": "https://avatars0.githubusercontent.com/u/6589896?v=4", - "profile": "https://github.com/sjmann", - "contributions": [ - "code", - "content" - ] - }, - { - "login": "Tarnadas", - "name": "Mario Reder", - "avatar_url": "https://avatars2.githubusercontent.com/u/5855071?v=4", - "profile": "https://smmdb.net/", - "contributions": [ - "code", - "content" - ] - }, - { - "login": "sl4m", - "name": "skim", - "avatar_url": "https://avatars0.githubusercontent.com/u/47347?v=4", - "profile": "https://keybase.io/skim", - "contributions": [ - "code" - ] - }, - { - "login": "sanjaykdragon", - "name": "Sanjay K", - "avatar_url": "https://avatars1.githubusercontent.com/u/10261698?v=4", - "profile": "https://github.com/sanjaykdragon", - "contributions": [ - "code", - "content" - ] - }, - { - "login": "crodjer", - "name": "Rohan Jain", - "avatar_url": "https://avatars1.githubusercontent.com/u/343499?v=4", - "profile": "http://www.rohanjain.in", - "contributions": [ - "code" - ] - }, - { - "login": "saidaspen", - "name": "Said Aspen", - "avatar_url": "https://avatars1.githubusercontent.com/u/7727687?v=4", - "profile": "https://www.saidaspen.se", - "contributions": [ - "code", - "content" - ] - }, - { - "login": "uce", - "name": "Ufuk Celebi", - "avatar_url": "https://avatars3.githubusercontent.com/u/1756620?v=4", - "profile": "https://github.com/uce", - "contributions": [ - "code" - ] - }, - { - "login": "lebedevsergey", - "name": "lebedevsergey", - "avatar_url": "https://avatars2.githubusercontent.com/u/7325764?v=4", - "profile": "https://github.com/lebedevsergey", - "contributions": [ - "doc" - ] - }, - { - "login": "avrong", - "name": "Aleksei Trifonov", - "avatar_url": "https://avatars2.githubusercontent.com/u/6342851?v=4", - "profile": "https://github.com/avrong", - "contributions": [ - "content" - ] - }, - { - "login": "Darrenmeehan", - "name": "Darren Meehan", - "avatar_url": "https://avatars2.githubusercontent.com/u/411136?v=4", - "profile": "https://drn.ie", - "contributions": [ - "content" - ] - }, - { - "login": "jihchi", - "name": "Jihchi Lee", - "avatar_url": "https://avatars1.githubusercontent.com/u/87983?v=4", - "profile": "https://github.com/jihchi", - "contributions": [ - "content" - ] - }, - { - "login": "bertonha", - "name": "Christofer Bertonha", - "avatar_url": "https://avatars3.githubusercontent.com/u/1225902?v=4", - "profile": "https://github.com/bertonha", - "contributions": [ - "content" - ] - }, - { - "login": "apatniv", - "name": "Vivek Bharath Akupatni", - "avatar_url": "https://avatars2.githubusercontent.com/u/22565917?v=4", - "profile": "https://github.com/apatniv", - "contributions": [ - "code", - "test" - ] - }, - { - "login": "DiD92", - "name": "Dídac Sementé Fernández", - "avatar_url": "https://avatars3.githubusercontent.com/u/6002416?v=4", - "profile": "https://github.com/DiD92", - "contributions": [ - "code", - "content" - ] - }, - { - "login": "wrobstory", - "name": "Rob Story", - "avatar_url": "https://avatars3.githubusercontent.com/u/2601457?v=4", - "profile": "https://github.com/wrobstory", - "contributions": [ - "code" - ] - }, - { - "login": "siobhanjacobson", - "name": "Siobhan Jacobson", - "avatar_url": "https://avatars2.githubusercontent.com/u/28983835?v=4", - "profile": "https://github.com/siobhanjacobson", - "contributions": [ - "code" - ] - }, - { - "login": "EvanCarroll", - "name": "Evan Carroll", - "avatar_url": "https://avatars2.githubusercontent.com/u/19922?v=4", - "profile": "https://www.linkedin.com/in/evancarroll/", - "contributions": [ - "content" - ] - }, - { - "login": "jmahmood", - "name": "Jawaad Mahmood", - "avatar_url": "https://avatars3.githubusercontent.com/u/95606?v=4", - "profile": "http://www.jawaadmahmood.com", - "contributions": [ - "content" - ] - }, - { - "login": "GaurangTandon", - "name": "Gaurang Tandon", - "avatar_url": "https://avatars1.githubusercontent.com/u/6308683?v=4", - "profile": "https://github.com/GaurangTandon", - "contributions": [ - "content" - ] - }, - { - "login": "dev-cyprium", - "name": "Stefan Kupresak", - "avatar_url": "https://avatars1.githubusercontent.com/u/6002628?v=4", - "profile": "https://github.com/dev-cyprium", - "contributions": [ - "content" - ] - }, - { - "login": "greg-el", - "name": "Greg Leonard", - "avatar_url": "https://avatars3.githubusercontent.com/u/45019882?v=4", - "profile": "https://github.com/greg-el", - "contributions": [ - "content" - ] - }, - { - "login": "ryanpcmcquen", - "name": "Ryan McQuen", - "avatar_url": "https://avatars3.githubusercontent.com/u/772937?v=4", - "profile": "https://ryanpcmcquen.org", - "contributions": [ - "code" - ] - }, - { - "login": "AnnikaCodes", - "name": "Annika", - "avatar_url": "https://avatars3.githubusercontent.com/u/56906084?v=4", - "profile": "https://github.com/AnnikaCodes", - "contributions": [ - "review" - ] - }, - { - "login": "darnuria", - "name": "Axel Viala", - "avatar_url": "https://avatars1.githubusercontent.com/u/2827553?v=4", - "profile": "https://darnuria.eu", - "contributions": [ - "code" - ] - }, - { - "login": "sazid", - "name": "Mohammed Sazid Al Rashid", - "avatar_url": "https://avatars1.githubusercontent.com/u/2370167?v=4", - "profile": "https://sazid.github.io", - "contributions": [ - "content", - "code" - ] - }, - { - "login": "seeplusplus", - "name": "Caleb Webber", - "avatar_url": "https://avatars1.githubusercontent.com/u/17479099?v=4", - "profile": "https://codingthemsoftly.com", - "contributions": [ - "maintenance" - ] - }, - { - "login": "pcn", - "name": "Peter N", - "avatar_url": "https://avatars2.githubusercontent.com/u/1056756?v=4", - "profile": "https://github.com/pcn", - "contributions": [ - "maintenance" - ] - }, - { - "login": "seancad", - "name": "seancad", - "avatar_url": "https://avatars1.githubusercontent.com/u/47405611?v=4", - "profile": "https://github.com/seancad", - "contributions": [ - "maintenance" - ] - }, - { - "login": "wsh", - "name": "Will Hayworth", - "avatar_url": "https://avatars3.githubusercontent.com/u/181174?v=4", - "profile": "http://willhayworth.com", - "contributions": [ - "content" - ] - }, - { - "login": "chrizel", - "name": "Christian Zeller", - "avatar_url": "https://avatars3.githubusercontent.com/u/20802?v=4", - "profile": "https://github.com/chrizel", - "contributions": [ - "content" - ] - }, - { - "login": "jfchevrette", - "name": "Jean-Francois Chevrette", - "avatar_url": "https://avatars.githubusercontent.com/u/3001?v=4", - "profile": "https://github.com/jfchevrette", - "contributions": [ - "content", - "code" - ] - }, - { - "login": "jbaber", - "name": "John Baber-Lucero", - "avatar_url": "https://avatars.githubusercontent.com/u/1908117?v=4", - "profile": "https://github.com/jbaber", - "contributions": [ - "content" - ] - }, - { - "login": "tal-zvon", - "name": "Tal", - "avatar_url": "https://avatars.githubusercontent.com/u/3195851?v=4", - "profile": "https://github.com/tal-zvon", - "contributions": [ - "content" - ] - }, - { - "login": "apogeeoak", - "name": "apogeeoak", - "avatar_url": "https://avatars.githubusercontent.com/u/59737221?v=4", - "profile": "https://github.com/apogeeoak", - "contributions": [ - "content", - "code" - ] - }, - { - "login": "Crell", - "name": "Larry Garfield", - "avatar_url": "https://avatars.githubusercontent.com/u/254863?v=4", - "profile": "http://www.garfieldtech.com/", - "contributions": [ - "content" - ] - }, - { - "login": "circumspect", - "name": "circumspect", - "avatar_url": "https://avatars.githubusercontent.com/u/40770208?v=4", - "profile": "https://github.com/circumspect", - "contributions": [ - "content" - ] - }, - { - "login": "cjwyett", - "name": "Cyrus Wyett", - "avatar_url": "https://avatars.githubusercontent.com/u/34195737?v=4", - "profile": "https://github.com/cjwyett", - "contributions": [ - "content" - ] - }, - { - "login": "cadolphs", - "name": "cadolphs", - "avatar_url": "https://avatars.githubusercontent.com/u/13894820?v=4", - "profile": "https://github.com/cadolphs", - "contributions": [ - "code" - ] - }, - { - "login": "hpwxf", - "name": "Pascal H.", - "avatar_url": "https://avatars.githubusercontent.com/u/26146722?v=4", - "profile": "https://www.haveneer.com", - "contributions": [ - "content" - ] - }, - { - "login": "chapeupreto", - "name": "Rod Elias", - "avatar_url": "https://avatars.githubusercontent.com/u/834048?v=4", - "profile": "https://twitter.com/chapeupreto", - "contributions": [ - "content" - ] - }, - { - "login": "blerchy", - "name": "Matt Lebl", - "avatar_url": "https://avatars.githubusercontent.com/u/2555355?v=4", - "profile": "https://github.com/blerchy", - "contributions": [ - "code" - ] - }, - { - "login": "flakolefluk", - "name": "Ignacio Le Fluk", - "avatar_url": "https://avatars.githubusercontent.com/u/11986564?v=4", - "profile": "http://flakolefluk.dev", - "contributions": [ - "content" - ] - }, - { - "login": "tlyu", - "name": "Taylor Yu", - "avatar_url": "https://avatars.githubusercontent.com/u/431873?v=4", - "profile": "https://github.com/tlyu", - "contributions": [ - "code", - "content" - ] - }, - { - "login": "Zerotask", - "name": "Patrick Hintermayer", - "avatar_url": "https://avatars.githubusercontent.com/u/20150243?v=4", - "profile": "https://zerotask.github.io", - "contributions": [ - "code" - ] - }, - { - "login": "arthas168", - "name": "Pete Pavlovski", - "avatar_url": "https://avatars.githubusercontent.com/u/32264020?v=4", - "profile": "https://petkopavlovski.com/", - "contributions": [ - "content" - ] - }, - { - "login": "k12ish", - "name": "k12ish", - "avatar_url": "https://avatars.githubusercontent.com/u/45272873?v=4", - "profile": "https://github.com/k12ish", - "contributions": [ - "content" - ] - }, - { - "login": "hongshaoyang", - "name": "Shao Yang Hong", - "avatar_url": "https://avatars.githubusercontent.com/u/19281800?v=4", - "profile": "https://github.com/hongshaoyang", - "contributions": [ - "content" - ] - }, - { - "login": "bmacer", - "name": "Brandon Macer", - "avatar_url": "https://avatars.githubusercontent.com/u/13931806?v=4", - "profile": "https://github.com/bmacer", - "contributions": [ - "content" - ] - }, - { - "login": "stoiandan", - "name": "Stoian Dan", - "avatar_url": "https://avatars.githubusercontent.com/u/10388612?v=4", - "profile": "https://github.com/stoiandan", - "contributions": [ - "content" - ] - }, - { - "login": "PiDelport", - "name": "Pi Delport", - "avatar_url": "https://avatars.githubusercontent.com/u/630271?v=4", - "profile": "https://about.me/pjdelport", - "contributions": [ - "content" - ] - }, - { - "login": "sateeshkumarb", - "name": "Sateesh ", - "avatar_url": "https://avatars.githubusercontent.com/u/429263?v=4", - "profile": "https://github.com/sateeshkumarb", - "contributions": [ - "code", - "content" - ] - }, - { - "login": "kayuapi", - "name": "ZC", - "avatar_url": "https://avatars.githubusercontent.com/u/10304328?v=4", - "profile": "https://github.com/kayuapi", - "contributions": [ - "content" - ] - }, - { - "login": "hyperparabolic", - "name": "hyperparabolic", - "avatar_url": "https://avatars.githubusercontent.com/u/12348474?v=4", - "profile": "https://github.com/hyperparabolic", - "contributions": [ - "code" - ] - }, - { - "login": "kolbma", - "name": "arlecchino", - "avatar_url": "https://avatars.githubusercontent.com/u/5228369?v=4", - "profile": "https://www.net4visions.at", - "contributions": [ - "doc" - ] - }, - { - "login": "jazzplato", - "name": "Richthofen", - "avatar_url": "https://avatars.githubusercontent.com/u/7576730?v=4", - "profile": "https://richthofen.io/", - "contributions": [ - "code" - ] - }, - { - "login": "cseltol", - "name": "Ivan Nerazumov", - "avatar_url": "https://avatars.githubusercontent.com/u/64264529?v=4", - "profile": "https://github.com/cseltol", - "contributions": [ - "doc" - ] - }, - { - "login": "lauralindzey", - "name": "lauralindzey", - "avatar_url": "https://avatars.githubusercontent.com/u/65185744?v=4", - "profile": "https://github.com/lauralindzey", - "contributions": [ - "doc" - ] - }, - { - "login": "sinharaksh1t", - "name": "Rakshit Sinha", - "avatar_url": "https://avatars.githubusercontent.com/u/28585848?v=4", - "profile": "https://github.com/sinharaksh1t", - "contributions": [ - "content" - ] - }, - { - "login": "dbednar230", - "name": "Damian", - "avatar_url": "https://avatars.githubusercontent.com/u/54457902?v=4", - "profile": "https://github.com/dbednar230", - "contributions": [ - "content" - ] - }, - { - "login": "benarmstead", - "name": "Ben Armstead", - "avatar_url": "https://avatars.githubusercontent.com/u/70973680?v=4", - "profile": "https://benarmstead.co.uk", - "contributions": [ - "code" - ] - }, - { - "login": "anuk909", - "name": "anuk909", - "avatar_url": "https://avatars.githubusercontent.com/u/34924662?v=4", - "profile": "https://github.com/anuk909", - "contributions": [ - "content", - "code" - ] - }, - { - "login": "granddaifuku", - "name": "granddaifuku", - "avatar_url": "https://avatars.githubusercontent.com/u/49578068?v=4", - "profile": "https://granddaifuku.com/", - "contributions": [ - "content" - ] - }, - { - "login": "Weilet", - "name": "Weilet", - "avatar_url": "https://avatars.githubusercontent.com/u/32561597?v=4", - "profile": "https://weilet.me", - "contributions": [ - "content" - ] - }, - { - "login": "Millione", - "name": "LIU JIE", - "avatar_url": "https://avatars.githubusercontent.com/u/38575932?v=4", - "profile": "https://github.com/Millione", - "contributions": [ - "content" - ] - }, - { - "login": "abusch", - "name": "Antoine Büsch", - "avatar_url": "https://avatars.githubusercontent.com/u/506344?v=4", - "profile": "https://github.com/abusch", - "contributions": [ - "code" - ] - }, - { - "login": "frogtd", - "name": "frogtd", - "avatar_url": "https://avatars.githubusercontent.com/u/31412003?v=4", - "profile": "https://frogtd.com/", - "contributions": [ - "content" - ] - }, - { - "login": "EmisonLu", - "name": "Zhenghao Lu", - "avatar_url": "https://avatars.githubusercontent.com/u/54395432?v=4", - "profile": "https://github.com/EmisonLu", - "contributions": [ - "content" - ] - }, - { - "login": "fredr", - "name": "Fredrik Enestad", - "avatar_url": "https://avatars.githubusercontent.com/u/762956?v=4", - "profile": "https://soundtrackyourbrand.com", - "contributions": [ - "content" - ] - }, - { - "login": "xuesongbj", - "name": "xuesong", - "avatar_url": "https://avatars.githubusercontent.com/u/18476085?v=4", - "profile": "http://xuesong.pydevops.com", - "contributions": [ - "content" - ] - }, - { - "login": "MpdWalsh", - "name": "Michael Walsh", - "avatar_url": "https://avatars.githubusercontent.com/u/48160144?v=4", - "profile": "https://github.com/MpdWalsh", - "contributions": [ - "code" - ] - }, - { - "login": "alirezaghey", - "name": "alirezaghey", - "avatar_url": "https://avatars.githubusercontent.com/u/26653424?v=4", - "profile": "https://github.com/alirezaghey", - "contributions": [ - "content" - ] - }, - { - "login": "frvannes16", - "name": "Franklin van Nes", - "avatar_url": "https://avatars.githubusercontent.com/u/3188475?v=4", - "profile": "https://github.com/frvannes16", - "contributions": [ - "code" - ] - }, - { - "login": "nekonako", - "name": "nekonako", - "avatar_url": "https://avatars.githubusercontent.com/u/46141275?v=4", - "profile": "https://nekonako.github.io", - "contributions": [ - "code" - ] - }, - { - "login": "tan-zx", - "name": "ZX", - "avatar_url": "https://avatars.githubusercontent.com/u/67887489?v=4", - "profile": "https://github.com/tan-zx", - "contributions": [ - "content" - ] - }, - { - "login": "sundevilyang", - "name": "Yang Wen", - "avatar_url": "https://avatars.githubusercontent.com/u/1499214?v=4", - "profile": "https://github.com/sundevilyang", - "contributions": [ - "content" - ] - }, - { - "login": "highb", - "name": "Brandon High", - "avatar_url": "https://avatars.githubusercontent.com/u/759848?v=4", - "profile": "https://brandon-high.com", - "contributions": [ - "doc" - ] - }, - { - "login": "x-hgg-x", - "name": "x-hgg-x", - "avatar_url": "https://avatars.githubusercontent.com/u/39058530?v=4", - "profile": "https://github.com/x-hgg-x", - "contributions": [ - "code" - ] - }, - { - "login": "KisaragiEffective", - "name": "Kisaragi", - "avatar_url": "https://avatars.githubusercontent.com/u/48310258?v=4", - "profile": "http://kisaragieffective.github.io", - "contributions": [ - "doc" - ] - }, - { - "login": "Kallu-A", - "name": "Lucas Aries", - "avatar_url": "https://avatars.githubusercontent.com/u/73198738?v=4", - "profile": "https://github.com/Kallu-A", - "contributions": [ - "content" - ] - }, - { - "login": "ragreenburg", - "name": "ragreenburg", - "avatar_url": "https://avatars.githubusercontent.com/u/24358100?v=4", - "profile": "https://github.com/ragreenburg", - "contributions": [ - "content" - ] - }, - { - "login": "stevenfukase", - "name": "stevenfukase", - "avatar_url": "https://avatars.githubusercontent.com/u/66785624?v=4", - "profile": "https://github.com/stevenfukase", - "contributions": [ - "content" - ] - }, - { - "login": "J-S-Kim", - "name": "J-S-Kim", - "avatar_url": "https://avatars.githubusercontent.com/u/17569303?v=4", - "profile": "https://github.com/J-S-Kim", - "contributions": [ - "content" - ] - }, - { - "login": "Fointard", - "name": "Fointard", - "avatar_url": "https://avatars.githubusercontent.com/u/9333398?v=4", - "profile": "https://github.com/Fointard", - "contributions": [ - "content" - ] - }, - { - "login": "rytheo", - "name": "Ryan Lowe", - "avatar_url": "https://avatars.githubusercontent.com/u/22184325?v=4", - "profile": "https://github.com/rytheo", - "contributions": [ - "code" - ] - }, - { - "login": "cuishuang", - "name": "cui fliter", - "avatar_url": "https://avatars.githubusercontent.com/u/15921519?v=4", - "profile": "http://www.dashen.tech", - "contributions": [ - "content" - ] - }, - { - "login": "luskwater", - "name": "Ron Lusk", - "avatar_url": "https://avatars.githubusercontent.com/u/42529?v=4", - "profile": "https://github.com/luskwater", - "contributions": [ - "content" - ] - }, - { - "login": "liby", - "name": "Bryan Lee", - "avatar_url": "https://avatars.githubusercontent.com/u/38807139?v=4", - "profile": "http://liby.github.io/liby/", - "contributions": [ - "content" - ] - }, - { - "login": "nandajavarma", - "name": "Nandaja Varma", - "avatar_url": "https://avatars.githubusercontent.com/u/2624550?v=4", - "profile": "http://nandaja.space", - "contributions": [ - "doc" - ] - }, - { - "login": "merelymyself", - "name": "pwygab", - "avatar_url": "https://avatars.githubusercontent.com/u/88221256?v=4", - "profile": "https://github.com/merelymyself", - "contributions": [ - "code" - ] - }, - { - "login": "lucasgrvarela", - "name": "Lucas Grigolon Varela", - "avatar_url": "https://avatars.githubusercontent.com/u/37870368?v=4", - "profile": "http://linkedin.com/in/lucasgrvarela", - "contributions": [ - "content" - ] - }, - { - "login": "bufo24", - "name": "Bufo", - "avatar_url": "https://avatars.githubusercontent.com/u/32884105?v=4", - "profile": "https://github.com/bufo24", - "contributions": [ - "content" - ] - }, - { - "login": "jackos", - "name": "Jack Clayton", - "avatar_url": "https://avatars.githubusercontent.com/u/77730378?v=4", - "profile": "http://rustnote.com", - "contributions": [ - "code" - ] - }, - { - "login": "klkl0808", - "name": "Konstantin", - "avatar_url": "https://avatars.githubusercontent.com/u/24694249?v=4", - "profile": "https://github.com/klkl0808", - "contributions": [ - "content" - ] - }, - { - "login": "0pling", - "name": "0pling", - "avatar_url": "https://avatars.githubusercontent.com/u/104090344?v=4", - "profile": "https://github.com/0pling", - "contributions": [ - "content" - ] - }, - { - "login": "KatanaFluorescent", - "name": "KatanaFluorescent", - "avatar_url": "https://avatars.githubusercontent.com/u/60199077?v=4", - "profile": "https://github.com/KatanaFluorescent", - "contributions": [ - "code" - ] - }, - { - "login": "Drew-Morris", - "name": "Drew Morris", - "avatar_url": "https://avatars.githubusercontent.com/u/95818166?v=4", - "profile": "https://github.com/Drew-Morris", - "contributions": [ - "code" - ] - }, - { - "login": "camperdue42", - "name": "camperdue42", - "avatar_url": "https://avatars.githubusercontent.com/u/43047763?v=4", - "profile": "https://github.com/camperdue42", - "contributions": [ - "content" - ] - }, - { - "login": "YsuOS", - "name": "YsuOS", - "avatar_url": "https://avatars.githubusercontent.com/u/30138661?v=4", - "profile": "https://github.com/YsuOS", - "contributions": [ - "content" - ] - }, - { - "login": "icecream17", - "name": "Steven Nguyen", - "avatar_url": "https://avatars.githubusercontent.com/u/58114641?v=4", - "profile": "https://lichess.org/@/StevenEmily", - "contributions": [ - "content" - ] - }, - { - "login": "nacairns1", - "name": "nacairns1", - "avatar_url": "https://avatars.githubusercontent.com/u/94420090?v=4", - "profile": "https://noahcairns.dev", - "contributions": [ - "content" - ] - }, - { - "login": "pgjbz", - "name": "Paulo Gabriel Justino Bezerra", - "avatar_url": "https://avatars.githubusercontent.com/u/22059237?v=4", - "profile": "https://github.com/pgjbz", - "contributions": [ - "content" - ] - }, - { - "login": "jaystile", - "name": "Jason", - "avatar_url": "https://avatars.githubusercontent.com/u/46078028?v=4", - "profile": "https://github.com/jaystile", - "contributions": [ - "content" - ] - }, - { - "login": "exdx", - "name": "exdx", - "avatar_url": "https://avatars.githubusercontent.com/u/31546601?v=4", - "profile": "https://exdx.github.io", - "contributions": [ - "content" - ] - }, - { - "login": "Jzow", - "name": "James Zow", - "avatar_url": "https://avatars.githubusercontent.com/u/68860495?v=4", - "profile": "https://github.com/Jzow", - "contributions": [ - "content" - ] - }, - { - "login": "jayber", - "name": "James Bromley", - "avatar_url": "https://avatars.githubusercontent.com/u/2474334?v=4", - "profile": "https://jamesabromley.wordpress.com/", - "contributions": [ - "content" - ] - }, - { - "login": "swhiteCQC", - "name": "swhiteCQC", - "avatar_url": "https://avatars.githubusercontent.com/u/77438466?v=4", - "profile": "https://github.com/swhiteCQC", - "contributions": [ - "content" - ] - }, - { - "login": "neilpate", - "name": "Neil Pate", - "avatar_url": "https://avatars.githubusercontent.com/u/7802334?v=4", - "profile": "https://github.com/neilpate", - "contributions": [ - "content" - ] - }, - { - "login": "wojexe", - "name": "wojexe", - "avatar_url": "https://avatars.githubusercontent.com/u/21208490?v=4", - "profile": "https://wojexe.com", - "contributions": [ - "content" - ] - }, - { - "login": "Tostapunk", - "name": "Mattia Schiavon", - "avatar_url": "https://avatars.githubusercontent.com/u/25140297?v=4", - "profile": "https://github.com/Tostapunk", - "contributions": [ - "content" - ] - }, - { - "login": "PrettyWood", - "name": "Eric Jolibois", - "avatar_url": "https://avatars.githubusercontent.com/u/18406791?v=4", - "profile": "http://toucantoco.com", - "contributions": [ - "content" - ] - }, - { - "login": "EdwinChang24", - "name": "Edwin Chang", - "avatar_url": "https://avatars.githubusercontent.com/u/88263098?v=4", - "profile": "http://edwinchang.vercel.app", - "contributions": [ - "content" - ] - }, - { - "login": "saikatdas0790", - "name": "Saikat Das", - "avatar_url": "https://avatars.githubusercontent.com/u/7412443?v=4", - "profile": "https://saikat.dev/", - "contributions": [ - "content" - ] - }, - { - "login": "thatlittleboy", - "name": "Jeremy Goh", - "avatar_url": "https://avatars.githubusercontent.com/u/30731072?v=4", - "profile": "https://github.com/thatlittleboy", - "contributions": [ - "content" - ] - }, - { - "login": "Lioness100", - "name": "Lioness100", - "avatar_url": "https://avatars.githubusercontent.com/u/65814829?v=4", - "profile": "https://github.com/Lioness100", - "contributions": [ - "content" - ] - }, - { - "login": "tvkn", - "name": "Tristan Nicholls", - "avatar_url": "https://avatars.githubusercontent.com/u/79277926?v=4", - "profile": "https://github.com/tvkn", - "contributions": [ - "content" - ] - }, - { - "login": "clairew", - "name": "Claire", - "avatar_url": "https://avatars.githubusercontent.com/u/9344258?v=4", - "profile": "http://clairewang.net", - "contributions": [ - "content" - ] - }, - { - "login": "Mouwrice", - "name": "Maurice Van Wassenhove", - "avatar_url": "https://avatars.githubusercontent.com/u/56763273?v=4", - "profile": "https://github.com/Mouwrice", - "contributions": [ - "content" - ] - }, - { - "login": "johnmendel", - "name": "John Mendelewski", - "avatar_url": "https://avatars.githubusercontent.com/u/77524?v=4", - "profile": "http://jmthree.com", - "contributions": [ - "code" - ] - }, - { - "login": "brianfakhoury", - "name": "Brian Fakhoury", - "avatar_url": "https://avatars.githubusercontent.com/u/20828724?v=4", - "profile": "http://fakhoury.xyz", - "contributions": [ - "content" - ] - }, - { - "login": "markusboehme", - "name": "Markus Boehme", - "avatar_url": "https://avatars.githubusercontent.com/u/5074759?v=4", - "profile": "https://github.com/markusboehme", - "contributions": [ - "code" - ] - }, - { - "login": "nico-vromans", - "name": "Nico Vromans", - "avatar_url": "https://avatars.githubusercontent.com/u/48183857?v=4", - "profile": "https://github.com/nico-vromans", - "contributions": [ - "content" - ] - }, - { - "login": "vostok92", - "name": "vostok92", - "avatar_url": "https://avatars.githubusercontent.com/u/540339?v=4", - "profile": "https://github.com/vostok92", - "contributions": [ - "content" - ] - }, - { - "login": "magnusrodseth", - "name": "Magnus Rødseth", - "avatar_url": "https://avatars.githubusercontent.com/u/59113973?v=4", - "profile": "http://magnusrodseth.vercel.app", - "contributions": [ - "content" - ] - }, - { - "login": "rubiesonthesky", - "name": "rubiesonthesky", - "avatar_url": "https://avatars.githubusercontent.com/u/2591240?v=4", - "profile": "https://github.com/rubiesonthesky", - "contributions": [ - "content" - ] - }, - { - "login": "GabrielBianconi", - "name": "Gabriel Bianconi", - "avatar_url": "https://avatars.githubusercontent.com/u/1275491?v=4", - "profile": "http://www.gabrielbianconi.com/", - "contributions": [ - "content" - ] - }, - { - "login": "Kodylow", - "name": "Kody Low", - "avatar_url": "https://avatars.githubusercontent.com/u/74332828?v=4", - "profile": "https://github.com/Kodylow", - "contributions": [ - "content" - ] - }, - { - "login": "rzrymiak", - "name": "rzrymiak", - "avatar_url": "https://avatars.githubusercontent.com/u/106121613?v=4", - "profile": "https://github.com/rzrymiak", - "contributions": [ - "content" - ] - }, - { - "login": "miguelraz", - "name": "Miguel Raz Guzmán Macedo", - "avatar_url": "https://avatars.githubusercontent.com/u/13056181?v=4", - "profile": "https://github.com/miguelraz", - "contributions": [ - "content" - ] - }, - { - "login": "memark", - "name": "Magnus Markling", - "avatar_url": "https://avatars.githubusercontent.com/u/318504?v=4", - "profile": "https://github.com/memark", - "contributions": [ - "content" - ] - }, - { - "login": "gasparitiago", - "name": "Tiago De Gaspari", - "avatar_url": "https://avatars.githubusercontent.com/u/3237254?v=4", - "profile": "https://github.com/gasparitiago", - "contributions": [ - "content" - ] - }, - { - "login": "skaunov", - "name": "skaunov", - "avatar_url": "https://avatars.githubusercontent.com/u/65976143?v=4", - "profile": "https://github.com/skaunov", - "contributions": [ - "content" - ] - }, - { - "login": "cj81499", - "name": "Cal Jacobson", - "avatar_url": "https://avatars.githubusercontent.com/u/9152032?v=4", - "profile": "http://caljacobson.dev", - "contributions": [ - "content" - ] - }, - { - "login": "duchonic", - "name": "Duchoud Nicolas", - "avatar_url": "https://avatars.githubusercontent.com/u/34117620?v=4", - "profile": "https://github.com/duchonic", - "contributions": [ - "content" - ] - }, - { - "login": "gfaugere", - "name": "Gaëtan Faugère", - "avatar_url": "https://avatars.githubusercontent.com/u/11901979?v=4", - "profile": "https://github.com/gfaugere", - "contributions": [ - "tool" - ] - }, - { - "login": "bhbuehler", - "name": "bhbuehler", - "avatar_url": "https://avatars.githubusercontent.com/u/25541343?v=4", - "profile": "https://github.com/bhbuehler", - "contributions": [ - "content" - ] - }, - { - "login": "nyurik", - "name": "Yuri Astrakhan", - "avatar_url": "https://avatars.githubusercontent.com/u/1641515?v=4", - "profile": "https://github.com/nyurik", - "contributions": [ - "code" - ] - }, - { - "login": "azzamsa", - "name": "azzamsa", - "avatar_url": "https://avatars.githubusercontent.com/u/17734314?v=4", - "profile": "http://azzamsa.com", - "contributions": [ - "code" - ] - }, - { - "login": "mvanschellebeeck", - "name": "mvanschellebeeck", - "avatar_url": "https://avatars.githubusercontent.com/u/17671052?v=4", - "profile": "https://github.com/mvanschellebeeck", - "contributions": [ - "content" - ] - }, - { - "login": "aaarkid", - "name": "Arkid", - "avatar_url": "https://avatars.githubusercontent.com/u/39987510?v=4", - "profile": "https://github.com/aaarkid", - "contributions": [ - "content" - ] - }, - { - "login": "tfpk", - "name": "Tom Kunc", - "avatar_url": "https://avatars.githubusercontent.com/u/10906982?v=4", - "profile": "http://tfpk.dev", - "contributions": [ - "content" - ] - }, - { - "login": "mfurak", - "name": "Marek Furák", - "avatar_url": "https://avatars.githubusercontent.com/u/38523093?v=4", - "profile": "https://github.com/mfurak", - "contributions": [ - "content" - ] - }, - { - "login": "winterqt", - "name": "Winter", - "avatar_url": "https://avatars.githubusercontent.com/u/78392041?v=4", - "profile": "https://winter.cafe", - "contributions": [ - "code" - ] - }, - { - "login": "MoritzBoehme", - "name": "Moritz Böhme", - "avatar_url": "https://avatars.githubusercontent.com/u/42215704?v=4", - "profile": "https://moritzboeh.me", - "contributions": [ - "code" - ] - }, - { - "login": "craymel", - "name": "craymel", - "avatar_url": "https://avatars.githubusercontent.com/u/71062756?v=4", - "profile": "https://github.com/craymel", - "contributions": [ - "content" - ] - }, - { - "login": "tkburis", - "name": "TK Buristrakul", - "avatar_url": "https://avatars.githubusercontent.com/u/20501289?v=4", - "profile": "https://github.com/tkburis", - "contributions": [ - "content" - ] - }, - { - "login": "HerschelW", - "name": "Kent Worthington", - "avatar_url": "https://avatars.githubusercontent.com/u/17935816?v=4", - "profile": "https://github.com/HerschelW", - "contributions": [ - "content" - ] - }, - { - "login": "seporterfield", - "name": "seporterfield", - "avatar_url": "https://avatars.githubusercontent.com/u/107010978?v=4", - "profile": "https://github.com/seporterfield", - "contributions": [ - "content" - ] - }, - { - "login": "dbarrosop", - "name": "David Barroso", - "avatar_url": "https://avatars.githubusercontent.com/u/6246622?v=4", - "profile": "https://www.linkedin.com/in/dbarrosop", - "contributions": [ - "infra" - ] - }, - { - "login": "tklauser", - "name": "Tobias Klauser", - "avatar_url": "https://avatars.githubusercontent.com/u/539708?v=4", - "profile": "https://distanz.ch", - "contributions": [ - "code" - ] - }, - { - "login": "0xMySt1c", - "name": "0xMySt1c", - "avatar_url": "https://avatars.githubusercontent.com/u/101825630?v=4", - "profile": "https://github.com/0xMySt1c", - "contributions": [ - "tool" - ] - }, - { - "login": "AxolotlTears", - "name": "Ten", - "avatar_url": "https://avatars.githubusercontent.com/u/87157047?v=4", - "profile": "https://github.com/AxolotlTears", - "contributions": [ - "code" - ] - }, - { - "login": "h4x5p4c3", - "name": "jones martin", - "avatar_url": "https://avatars.githubusercontent.com/u/66133688?v=4", - "profile": "http://h4x5p4c3.xyz", - "contributions": [ - "content" - ] - }, - { - "login": "cloppingemu", - "name": "cloppingemu", - "avatar_url": "https://avatars.githubusercontent.com/u/12227963?v=4", - "profile": "https://github.com/cloppingemu", - "contributions": [ - "code" - ] - }, - { - "login": "kevwan", - "name": "Kevin Wan", - "avatar_url": "https://avatars.githubusercontent.com/u/1918356?v=4", - "profile": "http://github.com/zeromicro/go-zero", - "contributions": [ - "content" - ] - }, - { - "login": "wjwrh", - "name": "Ruby", - "avatar_url": "https://avatars.githubusercontent.com/u/43495006?v=4", - "profile": "http://kurowasaruby.cn", - "contributions": [ - "code" - ] - }, - { - "login": "alexandergill", - "name": "Alexander Gill", - "avatar_url": "https://avatars.githubusercontent.com/u/7033716?v=4", - "profile": "https://github.com/alexandergill", - "contributions": [ - "code" - ] - }, - { - "login": "kawaiiPlat", - "name": "Jarrod Sanders", - "avatar_url": "https://avatars.githubusercontent.com/u/50600614?v=4", - "profile": "https://www.linkedin.com/in/jarrod-sanders/", - "contributions": [ - "content" - ] - }, - { - "login": "platformer", - "name": "Andrew Sen", - "avatar_url": "https://avatars.githubusercontent.com/u/40146328?v=4", - "profile": "https://github.com/platformer", - "contributions": [ - "content" - ] - }, - { - "login": "grzegorz-zur", - "name": "Grzegorz Żur", - "avatar_url": "https://avatars.githubusercontent.com/u/5297583?v=4", - "profile": "https://grzegorz-zur.com/", - "contributions": [ - "content" - ] - }, - { - "login": "black-puppydog", - "name": "Daan Wynen", - "avatar_url": "https://avatars.githubusercontent.com/u/189241?v=4", - "profile": "https://github.com/black-puppydog", - "contributions": [ - "content" - ] - }, - { - "login": "Anush008", - "name": "Anush", - "avatar_url": "https://avatars.githubusercontent.com/u/46051506?v=4", - "profile": "https://github.com/Anush008", - "contributions": [ - "doc" - ] - }, - { - "login": "shgew", - "name": "Gleb Shevchenko", - "avatar_url": "https://avatars.githubusercontent.com/u/5584672?v=4", - "profile": "https://github.com/shgew", - "contributions": [ - "content" - ] - }, - { - "login": "mdmundo", - "name": "Edmundo Paulino", - "avatar_url": "https://avatars.githubusercontent.com/u/60408300?v=4", - "profile": "https://github.com/mdmundo", - "contributions": [ - "infra" - ] - }, - { - "login": "eroullit", - "name": "Emmanuel Roullit", - "avatar_url": "https://avatars.githubusercontent.com/u/301795?v=4", - "profile": "https://github.com/eroullit", - "contributions": [ - "infra" - ] - }, - { - "login": "nidhalmessaoudi", - "name": "Nidhal Messaoudi", - "avatar_url": "https://avatars.githubusercontent.com/u/63377412?v=4", - "profile": "https://nidhalmessaoudi.herokuapp.com", - "contributions": [ - "code" - ] - }, - { - "login": "MahdiBM", - "name": "Mahdi Bahrami", - "avatar_url": "https://avatars.githubusercontent.com/u/54685446?v=4", - "profile": "https://github.com/MahdiBM", - "contributions": [ - "tool" - ] - }, - { - "login": "Nagidal", - "name": "Nagidal", - "avatar_url": "https://avatars.githubusercontent.com/u/7075397?v=4", - "profile": "https://github.com/Nagidal", - "contributions": [ - "content" - ] - }, - { - "login": "adamhb123", - "name": "Adam Brewer", - "avatar_url": "https://avatars.githubusercontent.com/u/25161597?v=4", - "profile": "https://adabrew.com", - "contributions": [ - "content" - ] - }, - { - "login": "eugkhp", - "name": "Eugene", - "avatar_url": "https://avatars.githubusercontent.com/u/25910599?v=4", - "profile": "https://github.com/eugkhp", - "contributions": [ - "tool" - ] - }, - { - "login": "navicore", - "name": "Ed Sweeney", - "avatar_url": "https://avatars.githubusercontent.com/u/110999?v=4", - "profile": "https://social.linux.pizza/@navicore", - "contributions": [ - "content" - ] - }, - { - "login": "javihernant", - "name": "javihernant", - "avatar_url": "https://avatars.githubusercontent.com/u/73640929?v=4", - "profile": "https://github.com/javihernant", - "contributions": [ - "content" - ] - }, - { - "login": "VegardMatthey", - "name": "Vegard", - "avatar_url": "https://avatars.githubusercontent.com/u/59250656?v=4", - "profile": "https://github.com/VegardMatthey", - "contributions": [ - "content" - ] - }, - { - "login": "ryanwhitehouse", - "name": "Ryan Whitehouse", - "avatar_url": "https://avatars.githubusercontent.com/u/13400784?v=4", - "profile": "https://github.com/ryanwhitehouse", - "contributions": [ - "content" - ] - }, - { - "login": "guoard", - "name": "Ali Afsharzadeh", - "avatar_url": "https://avatars.githubusercontent.com/u/65511355?v=4", - "profile": "https://github.com/guoard", - "contributions": [ - "content" - ] - }, - { - "login": "keogami", - "name": "Keogami", - "avatar_url": "https://avatars.githubusercontent.com/u/41939011?v=4", - "profile": "http://keogami.ml", - "contributions": [ - "doc" - ] - }, - { - "login": "ahresse", - "name": "Alexandre Esse", - "avatar_url": "https://avatars.githubusercontent.com/u/28402488?v=4", - "profile": "https://github.com/ahresse", - "contributions": [ - "content" - ] - }, - { - "login": "sagarvora", - "name": "Sagar Vora", - "avatar_url": "https://avatars.githubusercontent.com/u/16315650?v=4", - "profile": "https://resilient.tech", - "contributions": [ - "content" - ] - }, - { - "login": "poneciak57", - "name": "Kacper Poneta", - "avatar_url": "https://avatars.githubusercontent.com/u/94321164?v=4", - "profile": "https://github.com/poneciak57", - "contributions": [ - "content" - ] - }, - { - "login": "ktheory", - "name": "Aaron Suggs", - "avatar_url": "https://avatars.githubusercontent.com/u/975?v=4", - "profile": "https://ktheory.com/", - "contributions": [ - "content" - ] - }, - { - "login": "alexwh", - "name": "Alex", - "avatar_url": "https://avatars.githubusercontent.com/u/1723612?v=4", - "profile": "https://github.com/alexwh", - "contributions": [ - "content" - ] - }, - { - "login": "stornquist", - "name": "Sebastian Törnquist", - "avatar_url": "https://avatars.githubusercontent.com/u/42915664?v=4", - "profile": "https://github.com/stornquist", - "contributions": [ - "content" - ] - }, - { - "login": "smlavine", - "name": "Sebastian LaVine", - "avatar_url": "https://avatars.githubusercontent.com/u/33563640?v=4", - "profile": "http://smlavine.com", - "contributions": [ - "code" - ] - }, - { - "login": "akgerber", - "name": "Alan Gerber", - "avatar_url": "https://avatars.githubusercontent.com/u/201313?v=4", - "profile": "http://www.alangerber.us", - "contributions": [ - "content" - ] - }, - { - "login": "esotuvaka", - "name": "Eric", - "avatar_url": "https://avatars.githubusercontent.com/u/104941850?v=4", - "profile": "http://esotuvaka.github.io", - "contributions": [ - "content" - ] - }, - { - "login": "az0977776", - "name": "Aaron Wang", - "avatar_url": "https://avatars.githubusercontent.com/u/9172038?v=4", - "profile": "https://github.com/az0977776", - "contributions": [ - "content" - ] - }, - { - "login": "nmay231", - "name": "Noah", - "avatar_url": "https://avatars.githubusercontent.com/u/35386821?v=4", - "profile": "https://github.com/nmay231", - "contributions": [ - "content" - ] - }, - { - "login": "rb5014", - "name": "rb5014", - "avatar_url": "https://avatars.githubusercontent.com/u/105397317?v=4", - "profile": "https://github.com/rb5014", - "contributions": [ - "content" - ] - }, - { - "login": "deedy5", - "name": "deedy5", - "avatar_url": "https://avatars.githubusercontent.com/u/65482418?v=4", - "profile": "https://github.com/deedy5", - "contributions": [ - "content" - ] - }, - { - "login": "lionel-rowe", - "name": "lionel-rowe", - "avatar_url": "https://avatars.githubusercontent.com/u/26078826?v=4", - "profile": "https://github.com/lionel-rowe", - "contributions": [ - "content" - ] - }, - { - "login": "Ben2917", - "name": "Ben", - "avatar_url": "https://avatars.githubusercontent.com/u/10279994?v=4", - "profile": "https://github.com/Ben2917", - "contributions": [ - "content" - ] - }, - { - "login": "b1ue64", - "name": "b1ue64", - "avatar_url": "https://avatars.githubusercontent.com/u/77976308?v=4", - "profile": "https://github.com/b1ue64", - "contributions": [ - "content" - ] - }, - { - "login": "lazywalker", - "name": "lazywalker", - "avatar_url": "https://avatars.githubusercontent.com/u/53956?v=4", - "profile": "https://github.com/lazywalker", - "contributions": [ - "content" - ] - }, - { - "login": "proofconstruction", - "name": "proofconstruction", - "avatar_url": "https://avatars.githubusercontent.com/u/74747193?v=4", - "profile": "https://github.com/proofconstruction", - "contributions": [ - "infra" - ] - }, - { - "login": "IVIURRAY", - "name": "IVIURRAY", - "avatar_url": "https://avatars.githubusercontent.com/u/16007179?v=4", - "profile": "https://www.youtube.com/channel/UCQCjA6qUutAtWqkCA4Z36CQ", - "contributions": [ - "content" - ] - }, - { - "login": "b-apperlo", - "name": "Bert Apperlo", - "avatar_url": "https://avatars.githubusercontent.com/u/91734527?v=4", - "profile": "https://github.com/b-apperlo", - "contributions": [ - "content" - ] - }, - { - "login": "FWDekker", - "name": "Florine W. Dekker", - "avatar_url": "https://avatars.githubusercontent.com/u/13442533?v=4", - "profile": "https://fwdekker.com/", - "contributions": [ - "content" - ] - }, - { - "login": "luhem7", - "name": "Mehul Gangavelli", - "avatar_url": "https://avatars.githubusercontent.com/u/4008215?v=4", - "profile": "https://github.com/luhem7", - "contributions": [ - "content" - ] - }, - { - "login": "Frosthage", - "name": "Mikael Frosthage", - "avatar_url": "https://avatars.githubusercontent.com/u/14823314?v=4", - "profile": "https://github.com/Frosthage", - "contributions": [ - "content" - ] - }, - { - "login": "robertefry", - "name": "Robert Fry", - "avatar_url": "https://avatars.githubusercontent.com/u/43712054?v=4", - "profile": "https://robertfry.xyz", - "contributions": [ - "content" - ] - }, - { - "login": "tajo48", - "name": "tajo48", - "avatar_url": "https://avatars.githubusercontent.com/u/55502906?v=4", - "profile": "https://github.com/tajo48", - "contributions": [ - "content" - ] - }, - { - "login": "novanish", - "name": "Anish", - "avatar_url": "https://avatars.githubusercontent.com/u/98446102?v=4", - "profile": "https://anishchhetri.com.np", - "contributions": [ - "content" - ] - }, - { - "login": "vnprc", - "name": "vnprc", - "avatar_url": "https://avatars.githubusercontent.com/u/9425366?v=4", - "profile": "https://github.com/vnprc", - "contributions": [ - "content" - ] - }, - { - "login": "jrcarl624", - "name": "Joshua Carlson", - "avatar_url": "https://avatars.githubusercontent.com/u/61999256?v=4", - "profile": "http://androecia.net", - "contributions": [ - "content" - ] - }, - { - "login": "johnDeSilencio", - "name": "Nicholas R. Smith", - "avatar_url": "https://avatars.githubusercontent.com/u/20136554?v=4", - "profile": "https://johndesilencio.me", - "contributions": [ - "code" - ] - }, - { - "login": "alexfertel", - "name": "Alexander González", - "avatar_url": "https://avatars.githubusercontent.com/u/22298999?v=4", - "profile": "https://alexfertel.me", - "contributions": [ - "content" - ] - }, - { - "login": "softarn", - "name": "Marcus Höjvall", - "avatar_url": "https://avatars.githubusercontent.com/u/517619?v=4", - "profile": "https://github.com/softarn", - "contributions": [ - "content" - ] - }, - { - "login": "barlevalon", - "name": "Alon Hearter", - "avatar_url": "https://avatars.githubusercontent.com/u/3397911?v=4", - "profile": "https://github.com/barlevalon", - "contributions": [ - "content" - ] - }, - { - "login": "shirts", - "name": "shirts", - "avatar_url": "https://avatars.githubusercontent.com/u/4952151?v=4", - "profile": "https://github.com/shirts", - "contributions": [ - "content" - ] - }, - { - "login": "eLVas", - "name": "Ivan Vasiunyk", - "avatar_url": "https://avatars.githubusercontent.com/u/6797156?v=4", - "profile": "https://github.com/eLVas", - "contributions": [ - "content" - ] - }, - { - "login": "mo8it", - "name": "Mo", - "avatar_url": "https://avatars.githubusercontent.com/u/76752051?v=4", - "profile": "https://mo8it.com", - "contributions": [ - "code" - ] - }, - { - "login": "x10an14", - "name": "x10an14", - "avatar_url": "https://avatars.githubusercontent.com/u/710608?v=4", - "profile": "https://github.com/x10an14", - "contributions": [ - "infra" - ] - }, - { - "login": "gabay", - "name": "Roi Gabay", - "avatar_url": "https://avatars.githubusercontent.com/u/5773610?v=4", - "profile": "https://github.com/gabay", - "contributions": [ - "content" - ] - }, - { - "login": "mkovaxx", - "name": "Máté Kovács", - "avatar_url": "https://avatars.githubusercontent.com/u/481354?v=4", - "profile": "https://github.com/mkovaxx", - "contributions": [ - "content" - ] - }, - { - "login": "szabgab", - "name": "Gábor Szabó", - "avatar_url": "https://avatars.githubusercontent.com/u/48833?v=4", - "profile": "https://szabgab.com/", - "contributions": [ - "content" - ] - }, - { - "login": "yamila-moreno", - "name": "Yamila Moreno", - "avatar_url": "https://avatars.githubusercontent.com/u/3340793?v=4", - "profile": "https://moduslaborandi.net", - "contributions": [ - "content" - ] - }, - { - "login": "willhack", - "name": "Will Hack", - "avatar_url": "https://avatars.githubusercontent.com/u/18036720?v=4", - "profile": "https://github.com/willhack", - "contributions": [ - "content" - ] - }, - { - "login": "bean5", - "name": "Michael", - "avatar_url": "https://avatars.githubusercontent.com/u/2052646?v=4", - "profile": "http://cancompute.tech", - "contributions": [ - "content" - ] - }, - { - "login": "pksadiq", - "name": "Mohammed Sadiq", - "avatar_url": "https://avatars.githubusercontent.com/u/1289514?v=4", - "profile": "https://www.sadiqpk.org", - "contributions": [ - "content" - ] - }, - { - "login": "Jak-Ch-ll", - "name": "Jakob", - "avatar_url": "https://avatars.githubusercontent.com/u/56225668?v=4", - "profile": "https://github.com/Jak-Ch-ll", - "contributions": [ - "content" - ] - }, - { - "login": "ob", - "name": "Oscar Bonilla", - "avatar_url": "https://avatars.githubusercontent.com/u/4950?v=4", - "profile": "http://oscarbonilla.com", - "contributions": [ - "content" - ] - }, - { - "login": "husjon", - "name": "Jon Erling Hustadnes", - "avatar_url": "https://avatars.githubusercontent.com/u/554229?v=4", - "profile": "https://github.com/husjon", - "contributions": [ - "content" - ] - }, - { - "login": "CobaltCause", - "name": "Charles Hall", - "avatar_url": "https://avatars.githubusercontent.com/u/7003738?v=4", - "profile": "https://github.com/CobaltCause", - "contributions": [ - "infra" - ] - }, - { - "login": "krmpotic", - "name": "Luka Krmpotić", - "avatar_url": "https://avatars.githubusercontent.com/u/10350645?v=4", - "profile": "https://github.com/krmpotic", - "contributions": [ - "content" - ] - }, - { - "login": "jurglic", - "name": "Jurglic", - "avatar_url": "https://avatars.githubusercontent.com/u/112600?v=4", - "profile": "https://github.com/jurglic", - "contributions": [ - "content" - ] - }, - { - "login": "OfirLauber", - "name": "Ofir Lauber", - "avatar_url": "https://avatars.githubusercontent.com/u/5631030?v=4", - "profile": "https://github.com/OfirLauber", - "contributions": [ - "content" - ] - }, - { - "login": "offbyone", - "name": "Chris Rose", - "avatar_url": "https://avatars.githubusercontent.com/u/181693?v=4", - "profile": "https://github.com/offbyone", - "contributions": [ - "infra" - ] - }, - { - "login": "dieterplex", - "name": "d1t2", - "avatar_url": "https://avatars.githubusercontent.com/u/507502?v=4", - "profile": "https://github.com/dieterplex", - "contributions": [ - "infra" - ] - }, - { - "login": "docwilco", - "name": "docwilco", - "avatar_url": "https://avatars.githubusercontent.com/u/66911096?v=4", - "profile": "https://github.com/docwilco", - "contributions": [ - "code" - ] - }, - { - "login": "matthewjnield", - "name": "Matt Nield", - "avatar_url": "https://avatars.githubusercontent.com/u/64328730?v=4", - "profile": "https://www.linkedin.com/in/matthew-nield1/", - "contributions": [ - "content" - ] - }, - { - "login": "TheBearodactyl", - "name": "The Bearodactyl", - "avatar_url": "https://avatars.githubusercontent.com/u/114454115?v=4", - "profile": "https://github.com/TheBearodactyl", - "contributions": [ - "code" - ] - }, - { - "login": "markgreene74", - "name": "markgreene74", - "avatar_url": "https://avatars.githubusercontent.com/u/18945890?v=4", - "profile": "https://github.com/markgreene74", - "contributions": [ - "code" - ] - }, - { - "login": "VeeDeltaVee", - "name": "Versha Dhankar", - "avatar_url": "https://avatars.githubusercontent.com/u/45564258?v=4", - "profile": "https://github.com/VeeDeltaVee", - "contributions": [ - "doc" - ] - }, - { - "login": "0atman", - "name": "Tristram Oaten", - "avatar_url": "https://avatars.githubusercontent.com/u/114097?v=4", - "profile": "http://0atman.com", - "contributions": [ - "content" - ] - }, - { - "login": "danieltinazzi", - "name": "Daniel Tinazzi", - "avatar_url": "https://avatars.githubusercontent.com/u/11833533?v=4", - "profile": "https://github.com/danieltinazzi", - "contributions": [ - "content" - ] - }, - { - "login": "raymon-roos", - "name": "Raymon Roos", - "avatar_url": "https://avatars.githubusercontent.com/u/38888470?v=4", - "profile": "https://github.com/raymon-roos", - "contributions": [ - "content" - ] - }, - { - "login": "matthri", - "name": "Matthias", - "avatar_url": "https://avatars.githubusercontent.com/u/67913999?v=4", - "profile": "https://github.com/matthri", - "contributions": [ - "code" - ] - }, - { - "login": "neuschaefer", - "name": "J. Neuschäfer", - "avatar_url": "https://avatars.githubusercontent.com/u/1021512?v=4", - "profile": "https://github.com/neuschaefer", - "contributions": [ - "code" - ] - }, - { - "login": "bastianpedersen", - "name": "Bastian Pedersen", - "avatar_url": "https://avatars.githubusercontent.com/u/58905488?v=4", - "profile": "https://scooterhacking.org", - "contributions": [ - "content" - ] - }, - { - "login": "gerases", - "name": "gerases", - "avatar_url": "https://avatars.githubusercontent.com/u/8953623?v=4", - "profile": "https://github.com/gerases", - "contributions": [ - "content" - ] - }, - { - "login": "AnonimAnonim2245", - "name": "Luca Plian", - "avatar_url": "https://avatars.githubusercontent.com/u/98339220?v=4", - "profile": "https://github.com/AnonimAnonim2245", - "contributions": [ - "code" - ] - }, - { - "login": "reifenrath-dev", - "name": "René Reifenrath", - "avatar_url": "https://avatars.githubusercontent.com/u/18126097?v=4", - "profile": "https://reifenrath.dev/", - "contributions": [ - "content" - ] - }, - { - "login": "peterneave", - "name": "Peter Neave", - "avatar_url": "https://avatars.githubusercontent.com/u/7982708?v=4", - "profile": "https://github.com/peterneave", - "contributions": [ - "infra" - ] - }, - { - "login": "JanB1", - "name": "Jan", - "avatar_url": "https://avatars.githubusercontent.com/u/5552248?v=4", - "profile": "http://www.janb1.com", - "contributions": [ - "content" - ] - }, - { - "login": "kylev", - "name": "Kyle VanderBeek", - "avatar_url": "https://avatars.githubusercontent.com/u/46888?v=4", - "profile": "http://www.kylev.com/", - "contributions": [ - "infra" - ] - }, - { - "login": "pavedroad", - "name": "pavedroad", - "avatar_url": "https://avatars.githubusercontent.com/u/138004431?v=4", - "profile": "https://github.com/pavedroad", - "contributions": [ - "content" - ] - }, - { - "login": "hyphena", - "name": "luna", - "avatar_url": "https://avatars.githubusercontent.com/u/26529488?v=4", - "profile": "https://github.com/hyphena", - "contributions": [ - "content" - ] - }, - { - "login": "evanmiller2112", - "name": "Evan Miller", - "avatar_url": "https://avatars.githubusercontent.com/u/28488957?v=4", - "profile": "https://github.com/evanmiller2112", - "contributions": [ - "content" - ] - }, - { - "login": "luvchurchill", - "name": "luvchurchill", - "avatar_url": "https://avatars.githubusercontent.com/u/46406654?v=4", - "profile": "https://github.com/luvchurchill", - "contributions": [ - "code" - ] - }, - { - "login": "LeverImmy", - "name": "Ze-en Xiong", - "avatar_url": "https://avatars.githubusercontent.com/u/47663913?v=4", - "profile": "https://leverimmy.top/", - "contributions": [ - "content" - ] - }, - { - "login": "parnavh", - "name": "Parnav Harinathan", - "avatar_url": "https://avatars.githubusercontent.com/u/45985534?v=4", - "profile": "https://github.com/parnavh", - "contributions": [ - "content" - ] - }, - { - "login": "0Ahmed-0", - "name": "0Ahmed-0", - "avatar_url": "https://avatars.githubusercontent.com/u/111569638?v=4", - "profile": "https://github.com/0Ahmed-0", - "contributions": [ - "content" - ] - }, - { - "login": "guizo792", - "name": "guizo792", - "avatar_url": "https://avatars.githubusercontent.com/u/95940388?v=4", - "profile": "https://github.com/guizo792", - "contributions": [ - "content" - ] - }, - { - "login": "kazu728", - "name": "Kazuki Matsuo", - "avatar_url": "https://avatars.githubusercontent.com/u/34614358?v=4", - "profile": "https://github.com/kazu728", - "contributions": [ - "code" - ] - }, - { - "login": "paul-leydier", - "name": "Paul Leydier", - "avatar_url": "https://avatars.githubusercontent.com/u/75126792?v=4", - "profile": "https://github.com/paul-leydier", - "contributions": [ - "doc" - ] - }, - { - "login": "wznmickey", - "name": "wznmickey", - "avatar_url": "https://avatars.githubusercontent.com/u/44784663?v=4", - "profile": "http://wznmickey.com", - "contributions": [ - "doc" - ] - }, - { - "login": "NicolasRoelandt", - "name": "NicolasRoelandt", - "avatar_url": "https://avatars.githubusercontent.com/u/8594193?v=4", - "profile": "https://github.com/NicolasRoelandt", - "contributions": [ - "doc" - ] - }, - { - "login": "jbouganim-parallel", - "name": "Josh Bouganim", - "avatar_url": "https://avatars.githubusercontent.com/u/150748285?v=4", - "profile": "https://github.com/jbouganim-parallel", - "contributions": [ - "code" - ] - }, - { - "login": "loshz", - "name": "Dan", - "avatar_url": "https://avatars.githubusercontent.com/u/3449337?v=4", - "profile": "https://loshz.com", - "contributions": [ - "code" - ] - }, - { - "login": "Selflocking", - "name": "YunShu", - "avatar_url": "https://avatars.githubusercontent.com/u/53544726?v=4", - "profile": "https://yunshu.site", - "contributions": [ - "content" - ] - } - ], - "contributorsPerLine": 8, - "projectName": "rustlings", - "projectOwner": "rust-lang", - "repoType": "github", - "repoHost": "https://github.com", - "skipCi": true, - "commitConvention": "angular", - "commitType": "docs" -} diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json deleted file mode 100644 index f25e8bd8..00000000 --- a/.devcontainer/devcontainer.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "image": "mcr.microsoft.com/devcontainers/rust:1", - "updateContentCommand": ["cargo", "build"], - "postAttachCommand": ["rustlings", "watch"], - "remoteEnv": { - "PATH": "${containerEnv:PATH}:${containerWorkspaceFolder}/target/debug" - } -} diff --git a/.gitattributes b/.gitattributes deleted file mode 100644 index efdba876..00000000 --- a/.gitattributes +++ /dev/null @@ -1,2 +0,0 @@ -* text=auto -*.sh text eol=lf diff --git a/.gitignore b/.gitignore index 32f3c77b..945382c3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,18 +1,23 @@ -*.swp +# Cargo target/ -**/*.rs.bk -.DS_Store -*.pdb -exercises/22_clippy/Cargo.toml -exercises/22_clippy/Cargo.lock -rust-project.json -.idea -.vscode/* -!.vscode/extensions.json -*.iml -*.o +Cargo.lock +!/Cargo.lock + +# State file +.rustlings-state.txt + +# oranda public/ +.netlify + +# OS +.DS_Store .direnv/ -# Local Netlify folder -.netlify +# Editor +*.swp +.idea +*.iml + +# Ignore file for editors like Helix +.ignore diff --git a/.gitpod.yml b/.gitpod.yml deleted file mode 100644 index 06919335..00000000 --- a/.gitpod.yml +++ /dev/null @@ -1,7 +0,0 @@ -tasks: - - init: /workspace/rustlings/install.sh - command: /workspace/.cargo/bin/rustlings watch - -vscode: - extensions: - - rust-lang.rust-analyzer@0.3.1348 diff --git a/.typos.toml b/.typos.toml new file mode 100644 index 00000000..a74498ab --- /dev/null +++ b/.typos.toml @@ -0,0 +1,7 @@ +[files] +extend-exclude = [ + "CHANGELOG.md", +] + +[default.extend-words] +"ratatui" = "ratatui" diff --git a/.vscode/extensions.json b/.vscode/extensions.json deleted file mode 100644 index b85de749..00000000 --- a/.vscode/extensions.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "recommendations": [ - "rust-lang.rust-analyzer" - ] -} diff --git a/AUTHORS.md b/AUTHORS.md deleted file mode 100644 index 341ba42e..00000000 --- a/AUTHORS.md +++ /dev/null @@ -1,398 +0,0 @@ -## Authors - -This file lists the people that have contributed to this project. - -Excluded from this list are @carols10cents and @diannasoreil, the principal -authors. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Carol (Nichols || Goulding)
Carol (Nichols || Goulding)

💻 🖋
QuietMisdreavus
QuietMisdreavus

💻 🖋
Robert M Lugg
Robert M Lugg

🖋
Hynek Schlawack
Hynek Schlawack

💻
Katharina Fey
Katharina Fey

💻
lukabavdaz
lukabavdaz

💻 🖋
Erik Vesteraas
Erik Vesteraas

💻
delet0r
delet0r

💻
Shaun Bennett
Shaun Bennett

💻
Andrew Bagshaw
Andrew Bagshaw

💻
Kyle Isom
Kyle Isom

💻
Colin Pitrat
Colin Pitrat

💻
Zac Anger
Zac Anger

💻
Matthias Geier
Matthias Geier

💻
Chris Pearce
Chris Pearce

💻
Yvan Sraka
Yvan Sraka

💻
Denys Smirnov
Denys Smirnov

💻
eddyp
eddyp

💻
Brian Kung
Brian Kung

💻 🖋
Russell
Russell

💻
Dan Wilhelm
Dan Wilhelm

📖
Jesse
Jesse

💻 🖋
Fredrik Jambrén
Fredrik Jambrén

💻
Pete McFarlane
Pete McFarlane

🖋
nkanderson
nkanderson

💻 🖋
Ajax M
Ajax M

📖
Dylan Nugent
Dylan Nugent

🖋
vyaslav
vyaslav

💻 🖋
George
George

💻
Thomas Holloway
Thomas Holloway

💻 🖋
Jubilee
Jubilee

💻
WofWca
WofWca

💻
Roberto Vidal
Roberto Vidal

💻 📖 🤔 🚧
Jens
Jens

📖
Rahat Ahmed
Rahat Ahmed

📖
Abdou Seck
Abdou Seck

💻 🖋 👀
Katie
Katie

💻
Socrates
Socrates

📖
gnodarse
gnodarse

🖋
Harrison Metzger
Harrison Metzger

💻
Torben Jonas
Torben Jonas

💻 🖋
Paul Bissex
Paul Bissex

📖
Steven Mann
Steven Mann

💻 🖋
Mario Reder
Mario Reder

💻 🖋
skim
skim

💻
Sanjay K
Sanjay K

💻 🖋
Rohan Jain
Rohan Jain

💻
Said Aspen
Said Aspen

💻 🖋
Ufuk Celebi
Ufuk Celebi

💻
lebedevsergey
lebedevsergey

📖
Aleksei Trifonov
Aleksei Trifonov

🖋
Darren Meehan
Darren Meehan

🖋
Jihchi Lee
Jihchi Lee

🖋
Christofer Bertonha
Christofer Bertonha

🖋
Vivek Bharath Akupatni
Vivek Bharath Akupatni

💻 ⚠️
Dídac Sementé Fernández
Dídac Sementé Fernández

💻 🖋
Rob Story
Rob Story

💻
Siobhan Jacobson
Siobhan Jacobson

💻
Evan Carroll
Evan Carroll

🖋
Jawaad Mahmood
Jawaad Mahmood

🖋
Gaurang Tandon
Gaurang Tandon

🖋
Stefan Kupresak
Stefan Kupresak

🖋
Greg Leonard
Greg Leonard

🖋
Ryan McQuen
Ryan McQuen

💻
Annika
Annika

👀
Axel Viala
Axel Viala

💻
Mohammed Sazid Al Rashid
Mohammed Sazid Al Rashid

🖋 💻
Caleb Webber
Caleb Webber

🚧
Peter N
Peter N

🚧
seancad
seancad

🚧
Will Hayworth
Will Hayworth

🖋
Christian Zeller
Christian Zeller

🖋
Jean-Francois Chevrette
Jean-Francois Chevrette

🖋 💻
John Baber-Lucero
John Baber-Lucero

🖋
Tal
Tal

🖋
apogeeoak
apogeeoak

🖋 💻
Larry Garfield
Larry Garfield

🖋
circumspect
circumspect

🖋
Cyrus Wyett
Cyrus Wyett

🖋
cadolphs
cadolphs

💻
Pascal H.
Pascal H.

🖋
Rod Elias
Rod Elias

🖋
Matt Lebl
Matt Lebl

💻
Ignacio Le Fluk
Ignacio Le Fluk

🖋
Taylor Yu
Taylor Yu

💻 🖋
Patrick Hintermayer
Patrick Hintermayer

💻
Pete Pavlovski
Pete Pavlovski

🖋
k12ish
k12ish

🖋
Shao Yang Hong
Shao Yang Hong

🖋
Brandon Macer
Brandon Macer

🖋
Stoian Dan
Stoian Dan

🖋
Pi Delport
Pi Delport

🖋
Sateesh
Sateesh

💻 🖋
ZC
ZC

🖋
hyperparabolic
hyperparabolic

💻
arlecchino
arlecchino

📖
Richthofen
Richthofen

💻
Ivan Nerazumov
Ivan Nerazumov

📖
lauralindzey
lauralindzey

📖
Rakshit Sinha
Rakshit Sinha

🖋
Damian
Damian

🖋
Ben Armstead
Ben Armstead

💻
anuk909
anuk909

🖋 💻
granddaifuku
granddaifuku

🖋
Weilet
Weilet

🖋
LIU JIE
LIU JIE

🖋
Antoine Büsch
Antoine Büsch

💻
frogtd
frogtd

🖋
Zhenghao Lu
Zhenghao Lu

🖋
Fredrik Enestad
Fredrik Enestad

🖋
xuesong
xuesong

🖋
Michael Walsh
Michael Walsh

💻
alirezaghey
alirezaghey

🖋
Franklin van Nes
Franklin van Nes

💻
nekonako
nekonako

💻
ZX
ZX

🖋
Yang Wen
Yang Wen

🖋
Brandon High
Brandon High

📖
x-hgg-x
x-hgg-x

💻
Kisaragi
Kisaragi

📖
Lucas Aries
Lucas Aries

🖋
ragreenburg
ragreenburg

🖋
stevenfukase
stevenfukase

🖋
J-S-Kim
J-S-Kim

🖋
Fointard
Fointard

🖋
Ryan Lowe
Ryan Lowe

💻
cui fliter
cui fliter

🖋
Ron Lusk
Ron Lusk

🖋
Bryan Lee
Bryan Lee

🖋
Nandaja Varma
Nandaja Varma

📖
pwygab
pwygab

💻
Lucas Grigolon Varela
Lucas Grigolon Varela

🖋
Bufo
Bufo

🖋
Jack Clayton
Jack Clayton

💻
Konstantin
Konstantin

🖋
0pling
0pling

🖋
KatanaFluorescent
KatanaFluorescent

💻
Drew Morris
Drew Morris

💻
camperdue42
camperdue42

🖋
YsuOS
YsuOS

🖋
Steven Nguyen
Steven Nguyen

🖋
nacairns1
nacairns1

🖋
Paulo Gabriel Justino Bezerra
Paulo Gabriel Justino Bezerra

🖋
Jason
Jason

🖋
exdx
exdx

🖋
James Zow
James Zow

🖋
James Bromley
James Bromley

🖋
swhiteCQC
swhiteCQC

🖋
Neil Pate
Neil Pate

🖋
wojexe
wojexe

🖋
Mattia Schiavon
Mattia Schiavon

🖋
Eric Jolibois
Eric Jolibois

🖋
Edwin Chang
Edwin Chang

🖋
Saikat Das
Saikat Das

🖋
Jeremy Goh
Jeremy Goh

🖋
Lioness100
Lioness100

🖋
Tristan Nicholls
Tristan Nicholls

🖋
Claire
Claire

🖋
Maurice Van Wassenhove
Maurice Van Wassenhove

🖋
John Mendelewski
John Mendelewski

💻
Brian Fakhoury
Brian Fakhoury

🖋
Markus Boehme
Markus Boehme

💻
Nico Vromans
Nico Vromans

🖋
vostok92
vostok92

🖋
Magnus Rødseth
Magnus Rødseth

🖋
rubiesonthesky
rubiesonthesky

🖋
Gabriel Bianconi
Gabriel Bianconi

🖋
Kody Low
Kody Low

🖋
rzrymiak
rzrymiak

🖋
Miguel Raz Guzmán Macedo
Miguel Raz Guzmán Macedo

🖋
Magnus Markling
Magnus Markling

🖋
Tiago De Gaspari
Tiago De Gaspari

🖋
skaunov
skaunov

🖋
Cal Jacobson
Cal Jacobson

🖋
Duchoud Nicolas
Duchoud Nicolas

🖋
Gaëtan Faugère
Gaëtan Faugère

🔧
bhbuehler
bhbuehler

🖋
Yuri Astrakhan
Yuri Astrakhan

💻
azzamsa
azzamsa

💻
mvanschellebeeck
mvanschellebeeck

🖋
Arkid
Arkid

🖋
Tom Kunc
Tom Kunc

🖋
Marek Furák
Marek Furák

🖋
Winter
Winter

💻
Moritz Böhme
Moritz Böhme

💻
craymel
craymel

🖋
TK Buristrakul
TK Buristrakul

🖋
Kent Worthington
Kent Worthington

🖋
seporterfield
seporterfield

🖋
David Barroso
David Barroso

🚇
Tobias Klauser
Tobias Klauser

💻
0xMySt1c
0xMySt1c

🔧
Ten
Ten

💻
jones martin
jones martin

🖋
cloppingemu
cloppingemu

💻
Kevin Wan
Kevin Wan

🖋
Ruby
Ruby

💻
Alexander Gill
Alexander Gill

💻
Jarrod Sanders
Jarrod Sanders

🖋
Andrew Sen
Andrew Sen

🖋
Grzegorz Żur
Grzegorz Żur

🖋
Daan Wynen
Daan Wynen

🖋
Anush
Anush

📖
Gleb Shevchenko
Gleb Shevchenko

🖋
Edmundo Paulino
Edmundo Paulino

🚇
Emmanuel Roullit
Emmanuel Roullit

🚇
Nidhal Messaoudi
Nidhal Messaoudi

💻
Mahdi Bahrami
Mahdi Bahrami

🔧
Nagidal
Nagidal

🖋
Adam Brewer
Adam Brewer

🖋
Eugene
Eugene

🔧
Ed Sweeney
Ed Sweeney

🖋
javihernant
javihernant

🖋
Vegard
Vegard

🖋
Ryan Whitehouse
Ryan Whitehouse

🖋
Ali Afsharzadeh
Ali Afsharzadeh

🖋
Keogami
Keogami

📖
Alexandre Esse
Alexandre Esse

🖋
Sagar Vora
Sagar Vora

🖋
Kacper Poneta
Kacper Poneta

🖋
Aaron Suggs
Aaron Suggs

🖋
Alex
Alex

🖋
Sebastian Törnquist
Sebastian Törnquist

🖋
Sebastian LaVine
Sebastian LaVine

💻
Alan Gerber
Alan Gerber

🖋
Eric
Eric

🖋
Aaron Wang
Aaron Wang

🖋
Noah
Noah

🖋
rb5014
rb5014

🖋
deedy5
deedy5

🖋
lionel-rowe
lionel-rowe

🖋
Ben
Ben

🖋
b1ue64
b1ue64

🖋
lazywalker
lazywalker

🖋
proofconstruction
proofconstruction

🚇
IVIURRAY
IVIURRAY

🖋
Bert Apperlo
Bert Apperlo

🖋
Florine W. Dekker
Florine W. Dekker

🖋
Mehul Gangavelli
Mehul Gangavelli

🖋
Mikael Frosthage
Mikael Frosthage

🖋
Robert Fry
Robert Fry

🖋
tajo48
tajo48

🖋
Anish
Anish

🖋
vnprc
vnprc

🖋
Joshua Carlson
Joshua Carlson

🖋
Nicholas R. Smith
Nicholas R. Smith

💻
Alexander González
Alexander González

🖋
Marcus Höjvall
Marcus Höjvall

🖋
Alon Hearter
Alon Hearter

🖋
shirts
shirts

🖋
Ivan Vasiunyk
Ivan Vasiunyk

🖋
Mo
Mo

💻
x10an14
x10an14

🚇
Roi Gabay
Roi Gabay

🖋
Máté Kovács
Máté Kovács

🖋
Gábor Szabó
Gábor Szabó

🖋
Yamila Moreno
Yamila Moreno

🖋
Will Hack
Will Hack

🖋
Michael
Michael

🖋
Mohammed Sadiq
Mohammed Sadiq

🖋
Jakob
Jakob

🖋
Oscar Bonilla
Oscar Bonilla

🖋
Jon Erling Hustadnes
Jon Erling Hustadnes

🖋
Charles Hall
Charles Hall

🚇
Luka Krmpotić
Luka Krmpotić

🖋
Jurglic
Jurglic

🖋
Ofir Lauber
Ofir Lauber

🖋
Chris Rose
Chris Rose

🚇
d1t2
d1t2

🚇
docwilco
docwilco

💻
Matt Nield
Matt Nield

🖋
The Bearodactyl
The Bearodactyl

💻
markgreene74
markgreene74

💻
Versha Dhankar
Versha Dhankar

📖
Tristram Oaten
Tristram Oaten

🖋
Daniel Tinazzi
Daniel Tinazzi

🖋
Raymon Roos
Raymon Roos

🖋
Matthias
Matthias

💻
J. Neuschäfer
J. Neuschäfer

💻
Bastian Pedersen
Bastian Pedersen

🖋
gerases
gerases

🖋
Luca Plian
Luca Plian

💻
René Reifenrath
René Reifenrath

🖋
Peter Neave
Peter Neave

🚇
Jan
Jan

🖋
Kyle VanderBeek
Kyle VanderBeek

🚇
pavedroad
pavedroad

🖋
luna
luna

🖋
Evan Miller
Evan Miller

🖋
luvchurchill
luvchurchill

💻
Ze-en Xiong
Ze-en Xiong

🖋
Parnav Harinathan
Parnav Harinathan

🖋
0Ahmed-0
0Ahmed-0

🖋
guizo792
guizo792

🖋
Kazuki Matsuo
Kazuki Matsuo

💻
Paul Leydier
Paul Leydier

📖
wznmickey
wznmickey

📖
NicolasRoelandt
NicolasRoelandt

📖
Josh Bouganim
Josh Bouganim

💻
Dan
Dan

💻
YunShu
YunShu

🖋
- - - - - - -This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome! diff --git a/CHANGELOG.md b/CHANGELOG.md index a199e4de..11502ed1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,72 @@ + + +## 6.0.0 (2024-07-03) + +This release is the result of a complete rewrite to deliver a ton of new features and improvements ✨ +The most important changes are highlighted below. + +### Installation + +The installation has been simplified a lot! +To install Rustlings after installing Rust, all what you need to do now is running the following command: + +```bash +cargo install rustlings +``` + +Yes, this means that Rustlings is now on [crates.io](https://crates.io/crates/rustlings) 🎉 + +You can read about the motivations of this change in [this issue](https://github.com/rust-lang/rustlings/issues/1919). + +### UI/UX + +- The UI is now responsive when the terminal is resized. +- The progress bar was moved to the bottom so that you can always see your progress and the current exercise to work on. +- The current exercise path is now a terminal link. It will open the exercise file in your default editor when you click on it. +- A small prompt is now always shown at the bottom. It allows you to choose an action by entering a character. For example, entering `h` will show you the hint of the current exercise. +- The comment "I AM NOT DONE!" doesn't exist anymore. Instead of needing to remove it to go to the next exercise, you need to enter `n` in the terminal. + +### List mode + +A list mode was added using [Ratatui](https://ratatui.rs). +You can enter it by entering `l` in the watch mode. +It offers the following features: + +- Browse all exercises and see their state (pending/done). +- Filter exercises based on their state (pending/done). +- Continue at another exercise. This allows you to skip some exercises or go back to previous ones. +- Reset an exercise so you can start over and revert your changes. + +### Solutions + +After finishing an exercise, a solution file will be available and Rustlings will show you its path in green. +This allows you to compare your solution with an idiomatic solution and maybe learn about other ways to solve a problem. + +While writing the solutions, all exercises have been polished 🌟 +For example, every exercise now contains `TODO` comments to highlight what the user needs to change and where. + +### LSP support out of the box + +Instead of creating a `project.json` file using `rustlings lsp`, Rustlings now works with a `Cargo.toml` file out of the box. +No actions are needed to activate the language server `rust-analyzer`. + +This should avoid issues related to the language server or to running exercises, especially the ones with Clippy. + +### Clippy + +Clippy lints are now shown on all exercises, not only the Clippy exercises 📎 +Make Clippy your friend from early on 🥰 + +### Third party exercises + +Rustlings now supports third-party exercises! + +Do you want to create your own set of Rustlings exercises to focus on some specific topic? +Or do you want to translate the original Rustlings exercises? +Then follow the link to the guide about [third-party exercises](THIRD_PARTY_EXERCISES.md)! + + ## 5.6.1 (2023-09-18) #### Changed @@ -15,6 +83,7 @@ - `enums3`: Fixed formatting with `rustfmt`. + ## 5.6.0 (2023-09-04) #### Added @@ -30,7 +99,7 @@ - Swapped the order of threads and smart pointer exercises. - Rewrote the CLI to use `clap` - it's matured much since we switched to `argh` :) - `structs3`: Switched from i32 to u32. -- `move_semantics`: Switched 1-4 to tests, and rewrote them to be way simpler, while still teaching about the same +- `move_semantics`: Switched 1-4 to tests, and rewrote them to be way simpler, while still teaching about the same concepts. #### Fixed @@ -55,6 +124,7 @@ - Improved CI workflows, we're now testing on multiple platforms at once. + ## 5.5.1 (2023-05-17) #### Fixed @@ -62,6 +132,7 @@ - Reverted `rust-project.json` path generation due to an upstream `rust-analyzer` fix. + ## 5.5.0 (2023-05-17) #### Added @@ -97,6 +168,7 @@ - Split quick installation section into two code blocks + ## 5.4.1 (2023-03-10) #### Changed diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4fc7fb79..95605f70 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,129 +1,61 @@ -## Contributing to Rustlings +# Contributing to Rustlings -First off, thanks for taking the time to contribute!! ❤️ +First off, thanks for taking the time to contribute! ❤️ -### Quick Reference +## Quick Reference -I want to... +I want to … -_add an exercise! ➡️ [read this](#addex) and then [open a Pull Request](#prs)_ +- _report a bug!_ ➡️ [open an issue](#issues) +- _fix a bug!_ ➡️ [open a pull request](#pull-requests) +- _implement a new feature!_ ➡️ [open an issue to discuss it first, then a pull request](#issues) +- _add an exercise!_ ➡️ [read this](#adding-an-exercise) +- _update an outdated exercise!_ ➡️ [open a pull request](#pull-requests) -_update an outdated exercise! ➡️ [open a Pull Request](#prs)_ - -_report a bug! ➡️ [open an Issue](#issues)_ - -_fix a bug! ➡️ [open a Pull Request](#prs)_ - -_implement a new feature! ➡️ [open an Issue to discuss it first, then a Pull Request](#issues)_ - - -### Working on the source code - -`rustlings` is basically a glorified `rustc` wrapper. Therefore the source code -isn't really that complicated since the bulk of the work is done by `rustc`. - - -### Adding an exercise - -The first step is to add the exercise! Name the file `exercises/yourTopic/yourTopicN.rs`, make sure to -put in some helpful links, and link to sections of the book in `exercises/yourTopic/README.md`. - -Next make sure it runs with `rustlings`. The exercise metadata is stored in `info.toml`, under the `exercises` array. The order of the `exercises` array determines the order the exercises are run by `rustlings verify` and `rustlings watch`. - -Add the metadata for your exercise in the correct order in the `exercises` array. If you are unsure of the correct ordering, add it at the bottom and ask in your pull request. The exercise metadata should contain the following: -```diff - ... -+ [[exercises]] -+ name = "yourTopicN" -+ path = "exercises/yourTopic/yourTopicN.rs" -+ mode = "compile" -+ hint = """ -+ Some kind of useful hint for your exercise.""" - ... -``` - -The `mode` attribute decides whether Rustlings will only compile your exercise, or compile and test it. If you have tests to verify in your exercise, choose `test`, otherwise `compile`. If you're working on a Clippy exercise, use `mode = "clippy"`. - -That's all! Feel free to put up a pull request. - - -### Issues +## Issues You can open an issue [here](https://github.com/rust-lang/rustlings/issues/new). If you're reporting a bug, please include the output of the following commands: -- `rustc --version` +- `cargo --version` - `rustlings --version` - `ls -la` - Your OS name and version - -### Pull Requests +## Pull Requests -Opening a pull request is as easy as forking the repository and committing your -changes. There's a couple of things to watch out for: +You are welcome to open a pull request, but unless it is small and trivial, **please open an issue to discuss your idea first** 🙏🏼 -#### Write correct commit messages +Opening a pull request is as easy as forking the repository and committing your changes. +If you need any help with it or face any Git related problems, don't hesitate to ask for help 🤗 -We follow the [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) -specification. -This means that you have to format your commit messages in a specific way. Say -you're working on adding a new exercise called `foobar1.rs`. You could write -the following commit message: +It may take time to review your pull request. +Please be patient 😇 -``` -feat: add foobar1.rs exercise +When updating an exercise, check if its solution needs to be updated. + +## Adding An Exercise + +- Name the file `exercises/yourTopic/yourTopicN.rs`. +- Make sure to put in some helpful links, and link to sections of The Book in `exercises/yourTopic/README.md`. +- In the exercise, add a `// TODO: …` comment where user changes are required. +- Add a solution at `solutions/yourTopic/yourTopicN.rs` with comments explaining it. +- Add the [metadata for your exercise](#exercise-metadata) in the `rustlings-macros/info.toml` file. +- Make sure your exercise runs with `rustlings run yourTopicN`. +- [Open a pull request](#pull-requests). + +### Exercise Metadata + +The exercise metadata should contain the following: + +```toml +[[exercises]] +name = "yourTopicN" +dir = "yourTopic" +hint = """ +A useful (multi-line) hint for your exercise. +Include links to a section in The Book or a documentation page.""" ``` -If you're just fixing a bug, please use the `fix` type: - -``` -fix(verify): make sure verify doesn't self-destruct -``` - -The scope within the brackets is optional, but should be any of these: - -- `installation` (for the installation script) -- `cli` (for general CLI changes) -- `verify` (for the verification source file) -- `watch` (for the watch functionality source) -- `run` (for the run functionality source) -- `EXERCISENAME` (if you're changing a specific exercise, or set of exercises, - substitute them here) - -When the commit also happens to close an existing issue, link it in the message -body: - -``` -fix: update foobar - -closes #101029908 -``` - -If you're doing simple changes, like updating a book link, use `chore`: - -``` -chore: update exercise1.rs book link -``` - -If you're updating documentation, use `docs`: - -``` -docs: add more information to Readme -``` - -If, and only if, you're absolutely sure you want to make a breaking change -(please discuss this beforehand!), add an exclamation mark to the type and -explain the breaking change in the message body: - -``` -fix!: completely change verification - -BREAKING CHANGE: This has to be done because lorem ipsum dolor -``` - -#### Pull Request Workflow - -Once you open a Pull Request, it may be reviewed or labeled (or both) until -the maintainers accept your change. Please be patient, it may take some time -for this to happen! +If your exercise doesn't contain any test, add `test = false` to the exercise metadata. +But adding tests is recommended. diff --git a/Cargo.lock b/Cargo.lock index 3d0b16b2..f5908cc4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,18 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "ahash" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] + [[package]] name = "aho-corasick" version = "1.1.3" @@ -11,6 +23,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "allocator-api2" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" + [[package]] name = "anstream" version = "0.6.14" @@ -110,6 +128,21 @@ dependencies = [ "serde", ] +[[package]] +name = "cassowary" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" + +[[package]] +name = "castaway" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a17ed5635fc8536268e5d4de1e22e81ac34419e5f052d4d51f4e01dcc263fcc" +dependencies = [ + "rustversion", +] + [[package]] name = "cfg-if" version = "1.0.0" @@ -118,9 +151,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "clap" -version = "4.5.7" +version = "4.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5db83dced34638ad474f39f250d7fea9598bdd239eaced1bdf45d597da0f433f" +checksum = "84b3edb18336f4df585bc9aa31dd99c036dfa5dc5e9a2939a722a188f3a8970d" dependencies = [ "clap_builder", "clap_derive", @@ -128,9 +161,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.7" +version = "4.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7e204572485eb3fbf28f871612191521df159bc3e15a9f5064c66dba3a8c05f" +checksum = "c1c09dd5ada6c6c78075d6fd0da3f90d8080651e2d6cc8eb2f1aaa4034ced708" dependencies = [ "anstream", "anstyle", @@ -140,9 +173,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.5" +version = "4.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c780290ccf4fb26629baa7a1081e68ced113f1d3ec302fa5948f1c381ebf06c6" +checksum = "2bac35c6dafb060fd4d275d9a4ffae97917c13a6327903a8be2153cd964f7085" dependencies = [ "heck", "proc-macro2", @@ -163,16 +196,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422" [[package]] -name = "console" -version = "0.15.8" +name = "compact_str" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e1f83fc076bd6dd27517eacdf25fef6c4dfe5f1d7448bafaaf3a26f13b5e4eb" +checksum = "f86b9c4c00838774a6d902ef931eff7470720c51d90c2e32cfe15dc304737b3f" dependencies = [ - "encode_unicode", - "lazy_static", - "libc", - "unicode-width", - "windows-sys 0.52.0", + "castaway", + "cfg-if", + "itoa", + "ryu", + "static_assertions", ] [[package]] @@ -190,6 +223,31 @@ version = "0.8.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" +[[package]] +name = "crossterm" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f476fe445d41c9e991fd07515a6f463074b782242ccf4a5b7b1d1012e70824df" +dependencies = [ + "bitflags 2.6.0", + "crossterm_winapi", + "libc", + "mio", + "parking_lot", + "signal-hook", + "signal-hook-mio", + "winapi", +] + +[[package]] +name = "crossterm_winapi" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" +dependencies = [ + "winapi", +] + [[package]] name = "difflib" version = "0.4.0" @@ -208,28 +266,12 @@ version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" -[[package]] -name = "encode_unicode" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" - [[package]] name = "equivalent" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" -[[package]] -name = "errno" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" -dependencies = [ - "libc", - "windows-sys 0.52.0", -] - [[package]] name = "filetime" version = "0.2.23" @@ -238,7 +280,7 @@ checksum = "1ee447700ac8aa0b2f2bd7bc4462ad686ba06baa6727ac149a2d6277f0d240fd" dependencies = [ "cfg-if", "libc", - "redox_syscall", + "redox_syscall 0.4.1", "windows-sys 0.52.0", ] @@ -260,17 +302,15 @@ dependencies = [ "libc", ] -[[package]] -name = "glob" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" - [[package]] name = "hashbrown" version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash", + "allocator-api2", +] [[package]] name = "heck" @@ -278,15 +318,6 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" -[[package]] -name = "home" -version = "0.5.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" -dependencies = [ - "windows-sys 0.52.0", -] - [[package]] name = "indexmap" version = "2.2.6" @@ -297,19 +328,6 @@ dependencies = [ "hashbrown", ] -[[package]] -name = "indicatif" -version = "0.17.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "763a5a8f45087d6bcea4222e7b72c291a054edf80e4ef6efd2a4979878c7bea3" -dependencies = [ - "console", - "instant", - "number_prefix", - "portable-atomic", - "unicode-width", -] - [[package]] name = "inotify" version = "0.9.6" @@ -330,21 +348,30 @@ dependencies = [ "libc", ] -[[package]] -name = "instant" -version = "0.1.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" -dependencies = [ - "cfg-if", -] - [[package]] name = "is_terminal_polyfill" version = "1.70.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800" +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.11" @@ -371,12 +398,6 @@ dependencies = [ "libc", ] -[[package]] -name = "lazy_static" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" - [[package]] name = "libc" version = "0.2.155" @@ -384,16 +405,29 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" [[package]] -name = "linux-raw-sys" -version = "0.4.14" +name = "lock_api" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +dependencies = [ + "autocfg", + "scopeguard", +] [[package]] name = "log" -version = "0.4.21" +version = "0.4.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" + +[[package]] +name = "lru" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3262e75e648fce39813cb56ac41f3c3e3f65217ebf3844d818d1f9398cfb0dc" +dependencies = [ + "hashbrown", +] [[package]] name = "memchr" @@ -444,7 +478,6 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d40b221972a1fc5ef4d858a2f671fb34c75983eb385463dff3780eeff6a9d43" dependencies = [ - "crossbeam-channel", "log", "notify", ] @@ -459,16 +492,49 @@ dependencies = [ ] [[package]] -name = "number_prefix" -version = "0.4.0" +name = "once_cell" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" [[package]] -name = "portable-atomic" -version = "1.6.0" +name = "os_pipe" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7170ef9988bc169ba16dd36a7fa041e5c4cbeb6a35b76d4c03daded371eae7c0" +checksum = "29d73ba8daf8fac13b0501d1abeddcfe21ba7401ada61a819144b6c2a4f32209" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "parking_lot" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall 0.5.2", + "smallvec", + "windows-targets 0.52.5", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" [[package]] name = "predicates" @@ -518,6 +584,27 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "ratatui" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d16546c5b5962abf8ce6e2881e722b4e0ae3b6f1a08a26ae3573c55853ca68d3" +dependencies = [ + "bitflags 2.6.0", + "cassowary", + "compact_str", + "crossterm", + "itertools 0.13.0", + "lru", + "paste", + "stability", + "strum", + "strum_macros", + "unicode-segmentation", + "unicode-truncate", + "unicode-width", +] + [[package]] name = "redox_syscall" version = "0.4.1" @@ -527,6 +614,15 @@ dependencies = [ "bitflags 1.3.2", ] +[[package]] +name = "redox_syscall" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c82cf8cff14456045f55ec4241383baeff27af886adb72ffb2162f99911de0fd" +dependencies = [ + "bitflags 2.6.0", +] + [[package]] name = "regex" version = "1.10.5" @@ -556,39 +652,40 @@ version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" -[[package]] -name = "rustix" -version = "0.38.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" -dependencies = [ - "bitflags 2.6.0", - "errno", - "libc", - "linux-raw-sys", - "windows-sys 0.52.0", -] - [[package]] name = "rustlings" -version = "5.6.1" +version = "6.0.0-beta.10" dependencies = [ "anyhow", "assert_cmd", "clap", - "console", - "glob", - "indicatif", + "crossterm", + "hashbrown", "notify-debouncer-mini", + "os_pipe", "predicates", + "ratatui", + "rustlings-macros", "serde", "serde_json", - "shlex", "toml_edit", - "which", - "winnow", ] +[[package]] +name = "rustlings-macros" +version = "6.0.0-beta.10" +dependencies = [ + "quote", + "serde", + "toml_edit", +] + +[[package]] +name = "rustversion" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" + [[package]] name = "ryu" version = "1.0.18" @@ -604,6 +701,12 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + [[package]] name = "serde" version = "1.0.203" @@ -626,9 +729,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.118" +version = "1.0.120" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d947f6b3163d8857ea16c4fa0dd4840d52f3041039a85decd46867eb1abef2e4" +checksum = "4e0d21c9a8cae1235ad58a00c11cb40d4b1e5c784f1ef2c537876ed6ffd8b7c5" dependencies = [ "itoa", "ryu", @@ -645,10 +748,56 @@ dependencies = [ ] [[package]] -name = "shlex" -version = "1.3.0" +name = "signal-hook" +version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-mio" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29ad2e15f37ec9a6cc544097b78a1ec90001e9f71b81338ca39f430adaca99af" +dependencies = [ + "libc", + "mio", + "signal-hook", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +dependencies = [ + "libc", +] + +[[package]] +name = "smallvec" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" + +[[package]] +name = "stability" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ff9eaf853dec4c8802325d8b6d3dffa86cc707fd7a1a4cdbf416e13b061787a" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" [[package]] name = "strsim" @@ -656,6 +805,28 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "strum" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn", +] + [[package]] name = "syn" version = "2.0.68" @@ -701,6 +872,22 @@ version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" +[[package]] +name = "unicode-segmentation" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" + +[[package]] +name = "unicode-truncate" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5fbabedabe362c618c714dbefda9927b5afc8e2a8102f47f081089a9019226" +dependencies = [ + "itertools 0.12.1", + "unicode-width", +] + [[package]] name = "unicode-width" version = "0.1.13" @@ -713,6 +900,12 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + [[package]] name = "wait-timeout" version = "0.2.0" @@ -739,17 +932,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] -name = "which" -version = "6.0.1" +name = "winapi" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8211e4f58a2b2805adfbefbc07bab82958fc91e3836339b1ab7ae32465dce0d7" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" dependencies = [ - "either", - "home", - "rustix", - "winsafe", + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", ] +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + [[package]] name = "winapi-util" version = "0.1.8" @@ -759,6 +956,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + [[package]] name = "windows-sys" version = "0.48.0" @@ -908,7 +1111,21 @@ dependencies = [ ] [[package]] -name = "winsafe" -version = "0.0.19" +name = "zerocopy" +version = "0.7.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904" +checksum = "ae87e3fcd617500e5d106f0380cf7b77f3c6092aae37191433159dda23cfb087" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15e934569e47891f7d9411f1a451d947a60e000ab3bd24fbb970f000387d1b3b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/Cargo.toml b/Cargo.toml index 44409fcd..3455e2b4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,31 +1,72 @@ +[workspace] +resolver = "2" +exclude = [ + "tests/fixture/failure", + "tests/fixture/state", + "tests/fixture/success", + "dev", +] + +[workspace.package] +version = "6.0.0-beta.10" +authors = [ + "Liv ", + "Mo Bitar ", + # Alumni + "Carol (Nichols || Goulding) ", +] +repository = "https://github.com/rust-lang/rustlings" +license = "MIT" +edition = "2021" + +[workspace.dependencies] +serde = { version = "1.0.203", features = ["derive"] } +toml_edit = { version = "0.22.14", default-features = false, features = ["parse", "serde"] } + [package] name = "rustlings" description = "Small exercises to get you used to reading and writing Rust code!" -version = "5.6.1" -authors = [ - "Liv ", - "Carol (Nichols || Goulding) ", +version.workspace = true +authors.workspace = true +repository.workspace = true +license.workspace = true +edition.workspace = true +keywords = [ + "exercise", + "learning", +] +include = [ + "/src/", + "/exercises/", + "/solutions/", + # A symlink to be able to include `dev/Cargo.toml` although `dev` is excluded. + "/dev-Cargo.toml", + "/README.md", + "/LICENSE", ] -edition = "2021" [dependencies] anyhow = "1.0.86" -clap = { version = "4.5.7", features = ["derive"] } -console = "0.15.8" -indicatif = "0.17.8" -notify-debouncer-mini = "0.4.1" -serde_json = "1.0.118" -serde = { version = "1.0.203", features = ["derive"] } -shlex = "1.3.0" -toml_edit = { version = "0.22.14", default-features = false, features = ["parse", "serde"] } -which = "6.0.1" -winnow = "0.6.13" - -[[bin]] -name = "rustlings" -path = "src/main.rs" +clap = { version = "4.5.8", features = ["derive"] } +crossterm = "0.27.0" +hashbrown = "0.14.5" +notify-debouncer-mini = { version = "0.4.1", default-features = false } +os_pipe = "1.2.0" +ratatui = { version = "0.27.0", default-features = false, features = ["crossterm"] } +rustlings-macros = { path = "rustlings-macros", version = "=6.0.0-beta.10" } +serde_json = "1.0.120" +serde.workspace = true +toml_edit.workspace = true [dev-dependencies] assert_cmd = "2.0.14" -glob = "0.3.0" predicates = "3.1.0" + +[profile.release] +panic = "abort" + +[profile.dev] +panic = "abort" + +[package.metadata.release] +pre-release-hook = ["./release-hook.sh"] diff --git a/README.md b/README.md index 821d76cd..373b9c79 100644 --- a/README.md +++ b/README.md @@ -1,178 +1,143 @@
-# rustlings 🦀❤️ +# Rustlings 🦀❤️
-Greetings and welcome to `rustlings`. This project contains small exercises to get you used to reading and writing Rust code. This includes reading and responding to compiler messages! +Greetings and welcome to Rustlings. +This project contains small exercises to get you used to reading and writing Rust code. +This includes reading and responding to compiler messages! -Alternatively, for a first-time Rust learner, there are several other resources: +It is recommended to do the Rustlings exercises in parallel to reading [the official Rust book](https://doc.rust-lang.org/book/), the most comprehensive resource for learning Rust 📚️ -- [The Book](https://doc.rust-lang.org/book/index.html) - The most comprehensive resource for learning Rust, but a bit theoretical sometimes. You will be using this along with Rustlings! -- [Rust By Example](https://doc.rust-lang.org/rust-by-example/index.html) - Learn Rust by solving little exercises! It's almost like `rustlings`, but online +[Rust By Example](https://doc.rust-lang.org/rust-by-example/) is another recommended resource that you might find helpful. +It contains code examples and exercises similar to Rustlings, but online. ## Getting Started -_Note: If you're on MacOS, make sure you've installed Xcode and its developer tools by typing `xcode-select --install`._ -_Note: If you're on Linux, make sure you've installed gcc. Deb: `sudo apt install gcc`. Yum: `sudo yum -y install gcc`._ +### Installing Rust -You will need to have Rust installed. You can get it by visiting . This'll also install Cargo, Rust's package/project manager. +Before installing Rustlings, you need to have _Rust installed_. +Visit [www.rust-lang.org/tools/install](https://www.rust-lang.org/tools/install) for further instructions on installing Rust. +This'll also install _Cargo_, Rust's package/project manager. -## MacOS/Linux +> 🐧 If you're on Linux, make sure you've installed `gcc` (for a linker). +> +> Deb: `sudo apt install gcc`. +> Dnf: `sudo dnf install gcc`. -Just run: +> 🍎 If you're on MacOS, make sure you've installed Xcode and its developer tools by running `xcode-select --install`. + +### Installing Rustlings + +The following command will download and compile Rustlings: ```bash -curl -L https://raw.githubusercontent.com/rust-lang/rustlings/main/install.sh | bash +cargo install rustlings ``` -Or if you want it to be installed to a different path: +
+If the installation fails… (click to expand) + +- Make sure you have the latest Rust version by running `rustup update` +- Try adding the `--locked` flag: `cargo install rustlings --locked` +- Otherwise, please [report the issue](https://github.com/rust-lang/rustlings/issues/new) + +
+ +### Initialization + +After installing Rustlings, run the following command to initialize the `rustlings/` directory: ```bash -curl -L https://raw.githubusercontent.com/rust-lang/rustlings/main/install.sh | bash -s mypath/ +rustlings init ``` -This will install Rustlings and give you access to the `rustlings` command. Run it to get started! - -### Nix - -Basically: Clone the repository at the latest tag, finally run `nix develop` or `nix-shell`. +Now, go into the newly initialized directory and launch Rustlings for further instructions on getting started with the exercises: ```bash -# find out the latest version at https://github.com/rust-lang/rustlings/releases/latest (on edit 5.6.1) -git clone -b 5.6.1 --depth 1 https://github.com/rust-lang/rustlings -cd rustlings -# if nix version > 2.3 -nix develop -# if nix version <= 2.3 -nix-shell +cd rustlings/ +rustlings ``` -## Windows +## Working environment -In PowerShell (Run as Administrator), set `ExecutionPolicy` to `RemoteSigned`: +### Editor -```ps1 -Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser -``` +Our general recommendation is [VS Code](https://code.visualstudio.com/) with the [rust-analyzer plugin](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer). +But any editor that supports [rust-analyzer](https://rust-analyzer.github.io/) should be enough for working on the exercises. -Then, you can run: +### Terminal -```ps1 -Start-BitsTransfer -Source https://raw.githubusercontent.com/rust-lang/rustlings/main/install.ps1 -Destination $env:TMP/install_rustlings.ps1; Unblock-File $env:TMP/install_rustlings.ps1; Invoke-Expression $env:TMP/install_rustlings.ps1 -``` +While working with Rustlings, please use a modern terminal for the best user experience. +The default terminal on Linux and Mac should be sufficient. +On Windows, we recommend the [Windows Terminal](https://aka.ms/terminal). -To install Rustlings. Same as on MacOS/Linux, you will have access to the `rustlings` command after it. Keep in mind that this works best in PowerShell, and any other terminals may give you errors. - -If you get a permission denied message, you might have to exclude the directory where you cloned Rustlings in your antivirus. - -## Browser - -[![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/rust-lang/rustlings) - -[![Open Rustlings On Codespaces](https://github.com/codespaces/badge.svg)](https://github.com/codespaces/new/?repo=rust-lang%2Frustlings&ref=main) - -## Manually - -Basically: Clone the repository at the latest tag, run `cargo install --locked --path .`. - -```bash -# find out the latest version at https://github.com/rust-lang/rustlings/releases/latest (on edit 5.6.1) -git clone -b 5.6.1 --depth 1 https://github.com/rust-lang/rustlings -cd rustlings -cargo install --locked --force --path . -``` - -If there are installation errors, ensure that your toolchain is up to date. For the latest, run: - -```bash -rustup update -``` - -Then, same as above, run `rustlings` to get started. +If you use VS Code, the builtin terminal should also be fine. ## Doing exercises -The exercises are sorted by topic and can be found in the subdirectory `rustlings/exercises/`. For every topic there is an additional README file with some resources to get you started on the topic. We really recommend that you have a look at them before you start. +The exercises are sorted by topic and can be found in the subdirectory `exercises/`. +For every topic, there is an additional `README.md` file with some resources to get you started on the topic. +We highly recommend that you have a look at them before you start 📚️ -The task is simple. Most exercises contain an error that keeps them from compiling, and it's up to you to fix it! Some exercises are also run as tests, but rustlings handles them all the same. To run the exercises in the recommended order, execute: +Most exercises contain an error that keeps them from compiling, and it's up to you to fix it! +Some exercises contain tests that need to pass for the exercise to be done ✅ -```bash -rustlings watch -``` +Search for `TODO` and `todo!()` to find out what you need to change. +Ask for hints by entering `h` in the _watch mode_ 💡 -This will try to verify the completion of every exercise in a predetermined order (what we think is best for newcomers). It will also rerun automatically every time you change a file in the `exercises/` directory. If you want to only run it once, you can use: +### Watch Mode -```bash -rustlings verify -``` +After [initialization](#initialization), Rustlings can be launched by simply running the command `rustlings`. -This will do the same as watch, but it'll quit after running. +This will start the _watch mode_ which walks you through the exercises in a predefined order (what we think is best for newcomers). +It will rerun the current exercise automatically every time you change the exercise's file in the `exercises/` directory. -In case you want to go by your own order, or want to only verify a single exercise, you can run: +
+If detecting file changes in the exercises/ directory fails… (click to expand) -```bash -rustlings run myExercise1 -``` +> You can add the **`--manual-run`** flag (`rustlings --manual-run`) to manually rerun the current exercise by entering `r` in the watch mode. +> +> Please [report the issue](https://github.com/rust-lang/rustlings/issues/new) with some information about your operating system and whether you run Rustlings in a container or virtual machine (e.g. WSL). -Or simply use the following command to run the next unsolved exercise in the course: +
-```bash -rustlings run next -``` +### Exercise List -In case you get stuck, you can run the following command to get a hint for your -exercise: +In the [watch mode](#watch-mode) (after launching `rustlings`), you can enter `l` to open the interactive exercise list. -```bash -rustlings hint myExercise1 -``` +The list allows you to… -You can also get the hint for the next unsolved exercise with the following command: +- See the status of all exercises (done or pending) +- `c`: Continue at another exercise (temporarily skip some exercises or go back to a previous one) +- `r`: Reset status and file of an exercise (you need to _reload/reopen_ its file in your editor afterwards) -```bash -rustlings hint next -``` - -To check your progress, you can run the following command: - -```bash -rustlings list -``` - -## Testing yourself - -After every couple of sections, there will be a quiz that'll test your knowledge on a bunch of sections at once. These quizzes are found in `exercises/quizN.rs`. - -## Enabling `rust-analyzer` - -Run the command `rustlings lsp` which will generate a `rust-project.json` at the root of the project, this allows [rust-analyzer](https://rust-analyzer.github.io/) to parse each exercise. +See the footer of the list for all possible keys. ## Continuing On -Once you've completed Rustlings, put your new knowledge to good use! Continue practicing your Rust skills by building your own projects, contributing to Rustlings, or finding other open-source projects to contribute to. +Once you've completed Rustlings, put your new knowledge to good use! +Continue practicing your Rust skills by building your own projects, contributing to Rustlings, or finding other open-source projects to contribute to. + +## Third-Party Exercises + +Do you want to create your own set of Rustlings exercises to focus on some specific topic? +Or do you want to translate the original Rustlings exercises? +Then follow the link to the guide about [third-party exercises](THIRD_PARTY_EXERCISES.md)! ## Uninstalling Rustlings -If you want to remove Rustlings from your system, there are two steps. First, you'll need to remove the exercises folder that the install script created -for you: - -```bash -rm -rf rustlings # or your custom folder name, if you chose and or renamed it -``` - -Second, run `cargo uninstall` to remove the `rustlings` binary: +If you want to remove Rustlings from your system, run the following command: ```bash cargo uninstall rustlings ``` -Now you should be done! - ## Contributing -See [CONTRIBUTING.md](https://github.com/rust-lang/rustlings/blob/main/CONTRIBUTING.md). +See [CONTRIBUTING.md](https://github.com/rust-lang/rustlings/blob/main/CONTRIBUTING.md) 🔗 ## Contributors ✨ -Thanks goes to the wonderful people listed in [AUTHORS.md](https://github.com/rust-lang/rustlings/blob/main/AUTHORS.md) 🎉 +Thanks to [all the wonderful contributors](https://github.com/rust-lang/rustlings/graphs/contributors) 🎉 diff --git a/THIRD_PARTY_EXERCISES.md b/THIRD_PARTY_EXERCISES.md new file mode 100644 index 00000000..5c066941 --- /dev/null +++ b/THIRD_PARTY_EXERCISES.md @@ -0,0 +1,53 @@ +# Third-Party Exercises + +The support of Rustlings for third-party exercises allows you to create your own set of Rustlings exercises to focus on some specific topic. +You could also offer a translatation of the original Rustlings exercises as third-party exercises. + +## Getting started + +To create third-party exercises, install Rustlings and run `rustlings dev new PROJECT_NAME`. +This command will, similar to `cargo new PROJECT_NAME`, create a template directory called `PROJECT_NAME` with all what you need to get started. + +Read the comments in the generated `info.toml` file to understand its format. +It allows you to set a custom welcome and final message and specify the metadata of every exercise. + +## Create an exercise + +Here is an example of the metadata of one file: + +```toml +[[exercises]] +name = "intro1" +hint = """ +To finish this exercise, you need to … +This link might help you …""" +``` + +After entering this in `info.toml`, create the file `intro1.rs` in the `exercises/` directory. +The exercise needs to contain a `main` function, but it can be empty. +Adding tests is recommended. +Look at the official Rustlings exercises for inspiration. + +You can optionally add a solution file `intro1.rs` to the `solutions/` directory. + +Now, run `rustlings dev check`. +It will tell you about any issues with your exercises. +For example, it will tell you to run `rustlings dev update` to update the `Cargo.toml` file to include the new exercise `intro1`. + +`rustlings dev check` will also run your solutions (if you have any) to make sure that they run successfully. + +That's it! +You finished your first exercise 🎉 + +## Publish + +Now, add more exercises and publish them as a Git repository. + +Users just have to clone that repository and run `rustlings` in it to start working on your set of exercises just like the official ones. + +One difference to the official exercises is that the solution files will not be hidden until the user finishes an exercise. +But you can trust the users to not look at the solution too early 😉 + +## Share + +After publishing your set of exercises, open an issue or a pull request in the official Rustlings repository to link to your project in the README 😃 diff --git a/dev-Cargo.toml b/dev-Cargo.toml new file mode 120000 index 00000000..9230c2e9 --- /dev/null +++ b/dev-Cargo.toml @@ -0,0 +1 @@ +dev/Cargo.toml \ No newline at end of file diff --git a/dev/Cargo.toml b/dev/Cargo.toml new file mode 100644 index 00000000..7f3acb51 --- /dev/null +++ b/dev/Cargo.toml @@ -0,0 +1,197 @@ +# Don't edit the `bin` list manually! It is updated by `cargo run -- dev update`. This comment line will be stripped in `rustlings init`. +bin = [ + { name = "intro1", path = "../exercises/00_intro/intro1.rs" }, + { name = "intro1_sol", path = "../solutions/00_intro/intro1.rs" }, + { name = "intro2", path = "../exercises/00_intro/intro2.rs" }, + { name = "intro2_sol", path = "../solutions/00_intro/intro2.rs" }, + { name = "variables1", path = "../exercises/01_variables/variables1.rs" }, + { name = "variables1_sol", path = "../solutions/01_variables/variables1.rs" }, + { name = "variables2", path = "../exercises/01_variables/variables2.rs" }, + { name = "variables2_sol", path = "../solutions/01_variables/variables2.rs" }, + { name = "variables3", path = "../exercises/01_variables/variables3.rs" }, + { name = "variables3_sol", path = "../solutions/01_variables/variables3.rs" }, + { name = "variables4", path = "../exercises/01_variables/variables4.rs" }, + { name = "variables4_sol", path = "../solutions/01_variables/variables4.rs" }, + { name = "variables5", path = "../exercises/01_variables/variables5.rs" }, + { name = "variables5_sol", path = "../solutions/01_variables/variables5.rs" }, + { name = "variables6", path = "../exercises/01_variables/variables6.rs" }, + { name = "variables6_sol", path = "../solutions/01_variables/variables6.rs" }, + { name = "functions1", path = "../exercises/02_functions/functions1.rs" }, + { name = "functions1_sol", path = "../solutions/02_functions/functions1.rs" }, + { name = "functions2", path = "../exercises/02_functions/functions2.rs" }, + { name = "functions2_sol", path = "../solutions/02_functions/functions2.rs" }, + { name = "functions3", path = "../exercises/02_functions/functions3.rs" }, + { name = "functions3_sol", path = "../solutions/02_functions/functions3.rs" }, + { name = "functions4", path = "../exercises/02_functions/functions4.rs" }, + { name = "functions4_sol", path = "../solutions/02_functions/functions4.rs" }, + { name = "functions5", path = "../exercises/02_functions/functions5.rs" }, + { name = "functions5_sol", path = "../solutions/02_functions/functions5.rs" }, + { name = "if1", path = "../exercises/03_if/if1.rs" }, + { name = "if1_sol", path = "../solutions/03_if/if1.rs" }, + { name = "if2", path = "../exercises/03_if/if2.rs" }, + { name = "if2_sol", path = "../solutions/03_if/if2.rs" }, + { name = "if3", path = "../exercises/03_if/if3.rs" }, + { name = "if3_sol", path = "../solutions/03_if/if3.rs" }, + { name = "quiz1", path = "../exercises/quizzes/quiz1.rs" }, + { name = "quiz1_sol", path = "../solutions/quizzes/quiz1.rs" }, + { name = "primitive_types1", path = "../exercises/04_primitive_types/primitive_types1.rs" }, + { name = "primitive_types1_sol", path = "../solutions/04_primitive_types/primitive_types1.rs" }, + { name = "primitive_types2", path = "../exercises/04_primitive_types/primitive_types2.rs" }, + { name = "primitive_types2_sol", path = "../solutions/04_primitive_types/primitive_types2.rs" }, + { name = "primitive_types3", path = "../exercises/04_primitive_types/primitive_types3.rs" }, + { name = "primitive_types3_sol", path = "../solutions/04_primitive_types/primitive_types3.rs" }, + { name = "primitive_types4", path = "../exercises/04_primitive_types/primitive_types4.rs" }, + { name = "primitive_types4_sol", path = "../solutions/04_primitive_types/primitive_types4.rs" }, + { name = "primitive_types5", path = "../exercises/04_primitive_types/primitive_types5.rs" }, + { name = "primitive_types5_sol", path = "../solutions/04_primitive_types/primitive_types5.rs" }, + { name = "primitive_types6", path = "../exercises/04_primitive_types/primitive_types6.rs" }, + { name = "primitive_types6_sol", path = "../solutions/04_primitive_types/primitive_types6.rs" }, + { name = "vecs1", path = "../exercises/05_vecs/vecs1.rs" }, + { name = "vecs1_sol", path = "../solutions/05_vecs/vecs1.rs" }, + { name = "vecs2", path = "../exercises/05_vecs/vecs2.rs" }, + { name = "vecs2_sol", path = "../solutions/05_vecs/vecs2.rs" }, + { name = "move_semantics1", path = "../exercises/06_move_semantics/move_semantics1.rs" }, + { name = "move_semantics1_sol", path = "../solutions/06_move_semantics/move_semantics1.rs" }, + { name = "move_semantics2", path = "../exercises/06_move_semantics/move_semantics2.rs" }, + { name = "move_semantics2_sol", path = "../solutions/06_move_semantics/move_semantics2.rs" }, + { name = "move_semantics3", path = "../exercises/06_move_semantics/move_semantics3.rs" }, + { name = "move_semantics3_sol", path = "../solutions/06_move_semantics/move_semantics3.rs" }, + { name = "move_semantics4", path = "../exercises/06_move_semantics/move_semantics4.rs" }, + { name = "move_semantics4_sol", path = "../solutions/06_move_semantics/move_semantics4.rs" }, + { name = "move_semantics5", path = "../exercises/06_move_semantics/move_semantics5.rs" }, + { name = "move_semantics5_sol", path = "../solutions/06_move_semantics/move_semantics5.rs" }, + { name = "structs1", path = "../exercises/07_structs/structs1.rs" }, + { name = "structs1_sol", path = "../solutions/07_structs/structs1.rs" }, + { name = "structs2", path = "../exercises/07_structs/structs2.rs" }, + { name = "structs2_sol", path = "../solutions/07_structs/structs2.rs" }, + { name = "structs3", path = "../exercises/07_structs/structs3.rs" }, + { name = "structs3_sol", path = "../solutions/07_structs/structs3.rs" }, + { name = "enums1", path = "../exercises/08_enums/enums1.rs" }, + { name = "enums1_sol", path = "../solutions/08_enums/enums1.rs" }, + { name = "enums2", path = "../exercises/08_enums/enums2.rs" }, + { name = "enums2_sol", path = "../solutions/08_enums/enums2.rs" }, + { name = "enums3", path = "../exercises/08_enums/enums3.rs" }, + { name = "enums3_sol", path = "../solutions/08_enums/enums3.rs" }, + { name = "strings1", path = "../exercises/09_strings/strings1.rs" }, + { name = "strings1_sol", path = "../solutions/09_strings/strings1.rs" }, + { name = "strings2", path = "../exercises/09_strings/strings2.rs" }, + { name = "strings2_sol", path = "../solutions/09_strings/strings2.rs" }, + { name = "strings3", path = "../exercises/09_strings/strings3.rs" }, + { name = "strings3_sol", path = "../solutions/09_strings/strings3.rs" }, + { name = "strings4", path = "../exercises/09_strings/strings4.rs" }, + { name = "strings4_sol", path = "../solutions/09_strings/strings4.rs" }, + { name = "modules1", path = "../exercises/10_modules/modules1.rs" }, + { name = "modules1_sol", path = "../solutions/10_modules/modules1.rs" }, + { name = "modules2", path = "../exercises/10_modules/modules2.rs" }, + { name = "modules2_sol", path = "../solutions/10_modules/modules2.rs" }, + { name = "modules3", path = "../exercises/10_modules/modules3.rs" }, + { name = "modules3_sol", path = "../solutions/10_modules/modules3.rs" }, + { name = "hashmaps1", path = "../exercises/11_hashmaps/hashmaps1.rs" }, + { name = "hashmaps1_sol", path = "../solutions/11_hashmaps/hashmaps1.rs" }, + { name = "hashmaps2", path = "../exercises/11_hashmaps/hashmaps2.rs" }, + { name = "hashmaps2_sol", path = "../solutions/11_hashmaps/hashmaps2.rs" }, + { name = "hashmaps3", path = "../exercises/11_hashmaps/hashmaps3.rs" }, + { name = "hashmaps3_sol", path = "../solutions/11_hashmaps/hashmaps3.rs" }, + { name = "quiz2", path = "../exercises/quizzes/quiz2.rs" }, + { name = "quiz2_sol", path = "../solutions/quizzes/quiz2.rs" }, + { name = "options1", path = "../exercises/12_options/options1.rs" }, + { name = "options1_sol", path = "../solutions/12_options/options1.rs" }, + { name = "options2", path = "../exercises/12_options/options2.rs" }, + { name = "options2_sol", path = "../solutions/12_options/options2.rs" }, + { name = "options3", path = "../exercises/12_options/options3.rs" }, + { name = "options3_sol", path = "../solutions/12_options/options3.rs" }, + { name = "errors1", path = "../exercises/13_error_handling/errors1.rs" }, + { name = "errors1_sol", path = "../solutions/13_error_handling/errors1.rs" }, + { name = "errors2", path = "../exercises/13_error_handling/errors2.rs" }, + { name = "errors2_sol", path = "../solutions/13_error_handling/errors2.rs" }, + { name = "errors3", path = "../exercises/13_error_handling/errors3.rs" }, + { name = "errors3_sol", path = "../solutions/13_error_handling/errors3.rs" }, + { name = "errors4", path = "../exercises/13_error_handling/errors4.rs" }, + { name = "errors4_sol", path = "../solutions/13_error_handling/errors4.rs" }, + { name = "errors5", path = "../exercises/13_error_handling/errors5.rs" }, + { name = "errors5_sol", path = "../solutions/13_error_handling/errors5.rs" }, + { name = "errors6", path = "../exercises/13_error_handling/errors6.rs" }, + { name = "errors6_sol", path = "../solutions/13_error_handling/errors6.rs" }, + { name = "generics1", path = "../exercises/14_generics/generics1.rs" }, + { name = "generics1_sol", path = "../solutions/14_generics/generics1.rs" }, + { name = "generics2", path = "../exercises/14_generics/generics2.rs" }, + { name = "generics2_sol", path = "../solutions/14_generics/generics2.rs" }, + { name = "traits1", path = "../exercises/15_traits/traits1.rs" }, + { name = "traits1_sol", path = "../solutions/15_traits/traits1.rs" }, + { name = "traits2", path = "../exercises/15_traits/traits2.rs" }, + { name = "traits2_sol", path = "../solutions/15_traits/traits2.rs" }, + { name = "traits3", path = "../exercises/15_traits/traits3.rs" }, + { name = "traits3_sol", path = "../solutions/15_traits/traits3.rs" }, + { name = "traits4", path = "../exercises/15_traits/traits4.rs" }, + { name = "traits4_sol", path = "../solutions/15_traits/traits4.rs" }, + { name = "traits5", path = "../exercises/15_traits/traits5.rs" }, + { name = "traits5_sol", path = "../solutions/15_traits/traits5.rs" }, + { name = "quiz3", path = "../exercises/quizzes/quiz3.rs" }, + { name = "quiz3_sol", path = "../solutions/quizzes/quiz3.rs" }, + { name = "lifetimes1", path = "../exercises/16_lifetimes/lifetimes1.rs" }, + { name = "lifetimes1_sol", path = "../solutions/16_lifetimes/lifetimes1.rs" }, + { name = "lifetimes2", path = "../exercises/16_lifetimes/lifetimes2.rs" }, + { name = "lifetimes2_sol", path = "../solutions/16_lifetimes/lifetimes2.rs" }, + { name = "lifetimes3", path = "../exercises/16_lifetimes/lifetimes3.rs" }, + { name = "lifetimes3_sol", path = "../solutions/16_lifetimes/lifetimes3.rs" }, + { name = "tests1", path = "../exercises/17_tests/tests1.rs" }, + { name = "tests1_sol", path = "../solutions/17_tests/tests1.rs" }, + { name = "tests2", path = "../exercises/17_tests/tests2.rs" }, + { name = "tests2_sol", path = "../solutions/17_tests/tests2.rs" }, + { name = "tests3", path = "../exercises/17_tests/tests3.rs" }, + { name = "tests3_sol", path = "../solutions/17_tests/tests3.rs" }, + { name = "iterators1", path = "../exercises/18_iterators/iterators1.rs" }, + { name = "iterators1_sol", path = "../solutions/18_iterators/iterators1.rs" }, + { name = "iterators2", path = "../exercises/18_iterators/iterators2.rs" }, + { name = "iterators2_sol", path = "../solutions/18_iterators/iterators2.rs" }, + { name = "iterators3", path = "../exercises/18_iterators/iterators3.rs" }, + { name = "iterators3_sol", path = "../solutions/18_iterators/iterators3.rs" }, + { name = "iterators4", path = "../exercises/18_iterators/iterators4.rs" }, + { name = "iterators4_sol", path = "../solutions/18_iterators/iterators4.rs" }, + { name = "iterators5", path = "../exercises/18_iterators/iterators5.rs" }, + { name = "iterators5_sol", path = "../solutions/18_iterators/iterators5.rs" }, + { name = "box1", path = "../exercises/19_smart_pointers/box1.rs" }, + { name = "box1_sol", path = "../solutions/19_smart_pointers/box1.rs" }, + { name = "rc1", path = "../exercises/19_smart_pointers/rc1.rs" }, + { name = "rc1_sol", path = "../solutions/19_smart_pointers/rc1.rs" }, + { name = "arc1", path = "../exercises/19_smart_pointers/arc1.rs" }, + { name = "arc1_sol", path = "../solutions/19_smart_pointers/arc1.rs" }, + { name = "cow1", path = "../exercises/19_smart_pointers/cow1.rs" }, + { name = "cow1_sol", path = "../solutions/19_smart_pointers/cow1.rs" }, + { name = "threads1", path = "../exercises/20_threads/threads1.rs" }, + { name = "threads1_sol", path = "../solutions/20_threads/threads1.rs" }, + { name = "threads2", path = "../exercises/20_threads/threads2.rs" }, + { name = "threads2_sol", path = "../solutions/20_threads/threads2.rs" }, + { name = "threads3", path = "../exercises/20_threads/threads3.rs" }, + { name = "threads3_sol", path = "../solutions/20_threads/threads3.rs" }, + { name = "macros1", path = "../exercises/21_macros/macros1.rs" }, + { name = "macros1_sol", path = "../solutions/21_macros/macros1.rs" }, + { name = "macros2", path = "../exercises/21_macros/macros2.rs" }, + { name = "macros2_sol", path = "../solutions/21_macros/macros2.rs" }, + { name = "macros3", path = "../exercises/21_macros/macros3.rs" }, + { name = "macros3_sol", path = "../solutions/21_macros/macros3.rs" }, + { name = "macros4", path = "../exercises/21_macros/macros4.rs" }, + { name = "macros4_sol", path = "../solutions/21_macros/macros4.rs" }, + { name = "clippy1", path = "../exercises/22_clippy/clippy1.rs" }, + { name = "clippy1_sol", path = "../solutions/22_clippy/clippy1.rs" }, + { name = "clippy2", path = "../exercises/22_clippy/clippy2.rs" }, + { name = "clippy2_sol", path = "../solutions/22_clippy/clippy2.rs" }, + { name = "clippy3", path = "../exercises/22_clippy/clippy3.rs" }, + { name = "clippy3_sol", path = "../solutions/22_clippy/clippy3.rs" }, + { name = "using_as", path = "../exercises/23_conversions/using_as.rs" }, + { name = "using_as_sol", path = "../solutions/23_conversions/using_as.rs" }, + { name = "from_into", path = "../exercises/23_conversions/from_into.rs" }, + { name = "from_into_sol", path = "../solutions/23_conversions/from_into.rs" }, + { name = "from_str", path = "../exercises/23_conversions/from_str.rs" }, + { name = "from_str_sol", path = "../solutions/23_conversions/from_str.rs" }, + { name = "try_from_into", path = "../exercises/23_conversions/try_from_into.rs" }, + { name = "try_from_into_sol", path = "../solutions/23_conversions/try_from_into.rs" }, + { name = "as_ref_mut", path = "../exercises/23_conversions/as_ref_mut.rs" }, + { name = "as_ref_mut_sol", path = "../solutions/23_conversions/as_ref_mut.rs" }, +] + +[package] +name = "exercises" +edition = "2021" +# Don't publish the exercises on crates.io! +publish = false diff --git a/dev/rustlings-repo.txt b/dev/rustlings-repo.txt new file mode 100644 index 00000000..62793612 --- /dev/null +++ b/dev/rustlings-repo.txt @@ -0,0 +1 @@ +This file is used to check if the user tries to run Rustlings in the repository (the method before v6) diff --git a/exercises/00_intro/intro1.rs b/exercises/00_intro/intro1.rs index 5dd18b45..22544cd4 100644 --- a/exercises/00_intro/intro1.rs +++ b/exercises/00_intro/intro1.rs @@ -1,19 +1,10 @@ -// intro1.rs +// TODO: We sometimes encourage you to keep trying things on a given exercise, +// even after you already figured it out. If you got everything working and feel +// ready for the next exercise, enter `n` in the terminal. // -// About this `I AM NOT DONE` thing: -// We sometimes encourage you to keep trying things on a given exercise, even -// after you already figured it out. If you got everything working and feel -// ready for the next exercise, remove the `I AM NOT DONE` comment below. -// -// If you're running this using `rustlings watch`: The exercise file will be -// reloaded when you change one of the lines below! Try adding a `println!` -// line, or try changing what it outputs in your terminal. Try removing a -// semicolon and see what happens! -// -// Execute `rustlings hint intro1` or use the `hint` watch subcommand for a -// hint. - -// I AM NOT DONE +// The exercise file will be reloaded when you change one of the lines below! +// Try adding a new `println!`. +// Try removing a semicolon and see what happens in the terminal! fn main() { println!("Hello and"); @@ -29,13 +20,7 @@ fn main() { println!("or logic error. The central concept behind Rustlings is to fix these errors and"); println!("solve the exercises. Good luck!"); println!(); - println!("The source for this exercise is in `exercises/00_intro/intro1.rs`. Have a look!"); - println!( - "Going forward, the source of the exercises will always be in the success/failure output." - ); - println!(); - println!( - "If you want to use rust-analyzer, Rust's LSP implementation, make sure your editor is set" - ); - println!("up, and then run `rustlings lsp` before continuing.") + println!("The file of this exercise is `exercises/00_intro/intro1.rs`. Have a look!"); + println!("The current exercise path will be always shown under the progress bar."); + println!("You can click on the path to open the exercise file in your editor."); } diff --git a/exercises/00_intro/intro2.rs b/exercises/00_intro/intro2.rs index a28ad3dc..c6cb6451 100644 --- a/exercises/00_intro/intro2.rs +++ b/exercises/00_intro/intro2.rs @@ -1,12 +1,4 @@ -// intro2.rs -// -// Make the code print a greeting to the world. -// -// Execute `rustlings hint intro2` or use the `hint` watch subcommand for a -// hint. - -// I AM NOT DONE - fn main() { - printline!("Hello there!") + // TODO: Fix the code to print "Hello world!". + printline!("Hello world!"); } diff --git a/exercises/01_variables/variables1.rs b/exercises/01_variables/variables1.rs index b3e089a5..0a9e5548 100644 --- a/exercises/01_variables/variables1.rs +++ b/exercises/01_variables/variables1.rs @@ -1,13 +1,6 @@ -// variables1.rs -// -// Make me compile! -// -// Execute `rustlings hint variables1` or use the `hint` watch subcommand for a -// hint. - -// I AM NOT DONE - fn main() { + // TODO: Add missing keyword. x = 5; - println!("x has the value {}", x); + + println!("x has the value {x}"); } diff --git a/exercises/01_variables/variables2.rs b/exercises/01_variables/variables2.rs index e1c23edf..e2a36035 100644 --- a/exercises/01_variables/variables2.rs +++ b/exercises/01_variables/variables2.rs @@ -1,12 +1,7 @@ -// variables2.rs -// -// Execute `rustlings hint variables2` or use the `hint` watch subcommand for a -// hint. - -// I AM NOT DONE - fn main() { + // TODO: Change the line below to fix the compiler error. let x; + if x == 10 { println!("x is ten!"); } else { diff --git a/exercises/01_variables/variables3.rs b/exercises/01_variables/variables3.rs index 86bed419..06f35bb1 100644 --- a/exercises/01_variables/variables3.rs +++ b/exercises/01_variables/variables3.rs @@ -1,11 +1,6 @@ -// variables3.rs -// -// Execute `rustlings hint variables3` or use the `hint` watch subcommand for a -// hint. - -// I AM NOT DONE - fn main() { + // TODO: Change the line below to fix the compiler error. let x: i32; - println!("Number {}", x); + + println!("Number {x}"); } diff --git a/exercises/01_variables/variables4.rs b/exercises/01_variables/variables4.rs index 5394f394..6c138b18 100644 --- a/exercises/01_variables/variables4.rs +++ b/exercises/01_variables/variables4.rs @@ -1,13 +1,8 @@ -// variables4.rs -// -// Execute `rustlings hint variables4` or use the `hint` watch subcommand for a -// hint. - -// I AM NOT DONE - +// TODO: Fix the compiler error. fn main() { let x = 3; - println!("Number {}", x); - x = 5; // don't change this line - println!("Number {}", x); + println!("Number {x}"); + + x = 5; // Don't change this line + println!("Number {x}"); } diff --git a/exercises/01_variables/variables5.rs b/exercises/01_variables/variables5.rs index a29b38be..49db8e9e 100644 --- a/exercises/01_variables/variables5.rs +++ b/exercises/01_variables/variables5.rs @@ -1,13 +1,8 @@ -// variables5.rs -// -// Execute `rustlings hint variables5` or use the `hint` watch subcommand for a -// hint. - -// I AM NOT DONE - fn main() { - let number = "T-H-R-E-E"; // don't change this line - println!("Spell a Number : {}", number); - number = 3; // don't rename this variable - println!("Number plus two is : {}", number + 2); + let number = "T-H-R-E-E"; // Don't change this line + println!("Spell a number: {}", number); + + // TODO: Fix the compiler error by changing the line below without renaming the variable. + number = 3; + println!("Number plus two is: {}", number + 2); } diff --git a/exercises/01_variables/variables6.rs b/exercises/01_variables/variables6.rs index 853183ba..4a040fdd 100644 --- a/exercises/01_variables/variables6.rs +++ b/exercises/01_variables/variables6.rs @@ -1,11 +1,6 @@ -// variables6.rs -// -// Execute `rustlings hint variables6` or use the `hint` watch subcommand for a -// hint. - -// I AM NOT DONE - +// TODO: Change the line below to fix the compiler error. const NUMBER = 3; + fn main() { - println!("Number {}", NUMBER); + println!("Number: {NUMBER}"); } diff --git a/exercises/02_functions/functions1.rs b/exercises/02_functions/functions1.rs index 40ed9a07..a812c21b 100644 --- a/exercises/02_functions/functions1.rs +++ b/exercises/02_functions/functions1.rs @@ -1,10 +1,5 @@ -// functions1.rs -// -// Execute `rustlings hint functions1` or use the `hint` watch subcommand for a -// hint. - -// I AM NOT DONE +// TODO: Add some function with the name `call_me` without arguments or a return value. fn main() { - call_me(); + call_me(); // Don't change this line } diff --git a/exercises/02_functions/functions2.rs b/exercises/02_functions/functions2.rs index 5154f34d..2c773c6b 100644 --- a/exercises/02_functions/functions2.rs +++ b/exercises/02_functions/functions2.rs @@ -1,16 +1,10 @@ -// functions2.rs -// -// Execute `rustlings hint functions2` or use the `hint` watch subcommand for a -// hint. - -// I AM NOT DONE - -fn main() { - call_me(3); -} - +// TODO: Add the missing type of the argument `num` after the colon `:`. fn call_me(num:) { for i in 0..num { println!("Ring! Call number {}", i + 1); } } + +fn main() { + call_me(3); +} diff --git a/exercises/02_functions/functions3.rs b/exercises/02_functions/functions3.rs index 74f44d6d..5d5122af 100644 --- a/exercises/02_functions/functions3.rs +++ b/exercises/02_functions/functions3.rs @@ -1,16 +1,10 @@ -// functions3.rs -// -// Execute `rustlings hint functions3` or use the `hint` watch subcommand for a -// hint. - -// I AM NOT DONE - -fn main() { - call_me(); -} - fn call_me(num: u32) { for i in 0..num { println!("Ring! Call number {}", i + 1); } } + +fn main() { + // TODO: Fix the function call. + call_me(); +} diff --git a/exercises/02_functions/functions4.rs b/exercises/02_functions/functions4.rs index 77c4b2aa..b22bffda 100644 --- a/exercises/02_functions/functions4.rs +++ b/exercises/02_functions/functions4.rs @@ -1,21 +1,14 @@ -// functions4.rs -// // This store is having a sale where if the price is an even number, you get 10 -// Rustbucks off, but if it's an odd number, it's 3 Rustbucks off. (Don't worry -// about the function bodies themselves, we're only interested in the signatures -// for now. If anything, this is a good way to peek ahead to future exercises!) -// -// Execute `rustlings hint functions4` or use the `hint` watch subcommand for a -// hint. +// Rustbucks off, but if it's an odd number, it's 3 Rustbucks off. +// Don't worry about the function bodies themselves, we are only interested in +// the signatures for now. -// I AM NOT DONE - -fn main() { - let original_price = 51; - println!("Your sale price is {}", sale_price(original_price)); +fn is_even(num: i64) -> bool { + num % 2 == 0 } -fn sale_price(price: i32) -> { +// TODO: Fix the function signature. +fn sale_price(price: i64) -> { if is_even(price) { price - 10 } else { @@ -23,6 +16,7 @@ fn sale_price(price: i32) -> { } } -fn is_even(num: i32) -> bool { - num % 2 == 0 +fn main() { + let original_price = 51; + println!("Your sale price is {}", sale_price(original_price)); } diff --git a/exercises/02_functions/functions5.rs b/exercises/02_functions/functions5.rs index f1b63f48..34a2ac7d 100644 --- a/exercises/02_functions/functions5.rs +++ b/exercises/02_functions/functions5.rs @@ -1,15 +1,9 @@ -// functions5.rs -// -// Execute `rustlings hint functions5` or use the `hint` watch subcommand for a -// hint. - -// I AM NOT DONE - -fn main() { - let answer = square(3); - println!("The square of 3 is {}", answer); -} - +// TODO: Fix the function body without changing the signature. fn square(num: i32) -> i32 { num * num; } + +fn main() { + let answer = square(3); + println!("The square of 3 is {answer}"); +} diff --git a/exercises/03_if/if1.rs b/exercises/03_if/if1.rs index d2afccf8..e5a3c5a5 100644 --- a/exercises/03_if/if1.rs +++ b/exercises/03_if/if1.rs @@ -1,17 +1,15 @@ -// if1.rs -// -// Execute `rustlings hint if1` or use the `hint` watch subcommand for a hint. - -// I AM NOT DONE - -pub fn bigger(a: i32, b: i32) -> i32 { - // Complete this function to return the bigger number! +fn bigger(a: i32, b: i32) -> i32 { + // TODO: Complete this function to return the bigger number! // If both numbers are equal, any of them can be returned. // Do not use: // - another function call // - additional variables } +fn main() { + // You can optionally experiment here. +} + // Don't mind this for now :) #[cfg(test)] mod tests { diff --git a/exercises/03_if/if2.rs b/exercises/03_if/if2.rs index f512f13f..593a77a7 100644 --- a/exercises/03_if/if2.rs +++ b/exercises/03_if/if2.rs @@ -1,13 +1,5 @@ -// if2.rs -// -// Step 1: Make me compile! -// Step 2: Get the bar_for_fuzz and default_to_baz tests passing! -// -// Execute `rustlings hint if2` or use the `hint` watch subcommand for a hint. - -// I AM NOT DONE - -pub fn foo_if_fizz(fizzish: &str) -> &str { +// TODO: Fix the compiler error on this function. +fn foo_if_fizz(fizzish: &str) -> &str { if fizzish == "fizz" { "foo" } else { @@ -15,23 +7,29 @@ pub fn foo_if_fizz(fizzish: &str) -> &str { } } -// No test changes needed! +fn main() { + // You can optionally experiment here. +} + +// TODO: Read the tests to understand the desired behavior. +// Make all tests pass without changing them. #[cfg(test)] mod tests { use super::*; #[test] fn foo_for_fizz() { - assert_eq!(foo_if_fizz("fizz"), "foo") + // This means that calling `foo_if_fizz` with the argument "fizz" should return "foo". + assert_eq!(foo_if_fizz("fizz"), "foo"); } #[test] fn bar_for_fuzz() { - assert_eq!(foo_if_fizz("fuzz"), "bar") + assert_eq!(foo_if_fizz("fuzz"), "bar"); } #[test] fn default_to_baz() { - assert_eq!(foo_if_fizz("literally anything"), "baz") + assert_eq!(foo_if_fizz("literally anything"), "baz"); } } diff --git a/exercises/03_if/if3.rs b/exercises/03_if/if3.rs index 16962740..89164eb2 100644 --- a/exercises/03_if/if3.rs +++ b/exercises/03_if/if3.rs @@ -1,10 +1,5 @@ -// if3.rs -// -// Execute `rustlings hint if3` or use the `hint` watch subcommand for a hint. - -// I AM NOT DONE - -pub fn animal_habitat(animal: &str) -> &'static str { +fn animal_habitat(animal: &str) -> &str { + // TODO: Fix the compiler error in the statement below. let identifier = if animal == "crab" { 1 } else if animal == "gopher" { @@ -15,8 +10,8 @@ pub fn animal_habitat(animal: &str) -> &'static str { "Unknown" }; - // DO NOT CHANGE THIS STATEMENT BELOW - let habitat = if identifier == 1 { + // Don't change the expression below! + if identifier == 1 { "Beach" } else if identifier == 2 { "Burrow" @@ -24,12 +19,14 @@ pub fn animal_habitat(animal: &str) -> &'static str { "Desert" } else { "Unknown" - }; - - habitat + } } -// No test changes needed. +fn main() { + // You can optionally experiment here. +} + +// Don't change the tests! #[cfg(test)] mod tests { use super::*; diff --git a/exercises/04_primitive_types/primitive_types1.rs b/exercises/04_primitive_types/primitive_types1.rs index 36633400..84923c75 100644 --- a/exercises/04_primitive_types/primitive_types1.rs +++ b/exercises/04_primitive_types/primitive_types1.rs @@ -1,19 +1,14 @@ -// primitive_types1.rs -// -// Fill in the rest of the line that has code missing! No hints, there's no -// tricks, just get used to typing these :) - -// I AM NOT DONE +// Booleans (`bool`) fn main() { - // Booleans (`bool`) - let is_morning = true; if is_morning { println!("Good morning!"); } - let // Finish the rest of this line like the example! Or make it be false! + // TODO: Define a boolean variable with the name `is_evening` before the `if` statement below. + // The value of the variable should be the negation (opposite) of `is_morning`. + // let … if is_evening { println!("Good evening!"); } diff --git a/exercises/04_primitive_types/primitive_types2.rs b/exercises/04_primitive_types/primitive_types2.rs index f1616ed3..14018475 100644 --- a/exercises/04_primitive_types/primitive_types2.rs +++ b/exercises/04_primitive_types/primitive_types2.rs @@ -1,13 +1,6 @@ -// primitive_types2.rs -// -// Fill in the rest of the line that has code missing! No hints, there's no -// tricks, just get used to typing these :) - -// I AM NOT DONE +// Characters (`char`) fn main() { - // Characters (`char`) - // Note the _single_ quotes, these are different from the double quotes // you've been seeing around. let my_first_initial = 'C'; @@ -19,9 +12,12 @@ fn main() { println!("Neither alphabetic nor numeric!"); } - let // Finish this line like the example! What's your favorite character? - // Try a letter, try a number, try a special character, try a character - // from a different language than your own, try an emoji! + // TODO: Analogous to the example before, declare a variable called `your_character` + // below with your favorite character. + // Try a letter, try a digit (in single quotes), try a special character, try a character + // from a different language than your own, try an emoji 😉 + // let your_character = ''; + if your_character.is_alphabetic() { println!("Alphabetical!"); } else if your_character.is_numeric() { diff --git a/exercises/04_primitive_types/primitive_types3.rs b/exercises/04_primitive_types/primitive_types3.rs index 8b0de44e..9b79c0cf 100644 --- a/exercises/04_primitive_types/primitive_types3.rs +++ b/exercises/04_primitive_types/primitive_types3.rs @@ -1,19 +1,11 @@ -// primitive_types3.rs -// -// Create an array with at least 100 elements in it where the ??? is. -// -// Execute `rustlings hint primitive_types3` or use the `hint` watch subcommand -// for a hint. - -// I AM NOT DONE - fn main() { - let a = ??? + // TODO: Create an array called `a` with at least 100 elements in it. + // let a = ??? if a.len() >= 100 { println!("Wow, that's a big array!"); } else { println!("Meh, I eat arrays like that for breakfast."); - panic!("Array not big enough, more elements needed") + panic!("Array not big enough, more elements needed"); } } diff --git a/exercises/04_primitive_types/primitive_types4.rs b/exercises/04_primitive_types/primitive_types4.rs index d44d8776..16e4fd93 100644 --- a/exercises/04_primitive_types/primitive_types4.rs +++ b/exercises/04_primitive_types/primitive_types4.rs @@ -1,17 +1,16 @@ -// primitive_types4.rs -// -// Get a slice out of Array a where the ??? is so that the test passes. -// -// Execute `rustlings hint primitive_types4` or use the `hint` watch subcommand -// for a hint. - -// I AM NOT DONE - -#[test] -fn slice_out_of_array() { - let a = [1, 2, 3, 4, 5]; - - let nice_slice = ??? - - assert_eq!([2, 3, 4], nice_slice) +fn main() { + // You can optionally experiment here. +} + +#[cfg(test)] +mod tests { + #[test] + fn slice_out_of_array() { + let a = [1, 2, 3, 4, 5]; + + // TODO: Get a slice called `nice_slice` out of the array `a` so that the test passes. + // let nice_slice = ??? + + assert_eq!([2, 3, 4], nice_slice); + } } diff --git a/exercises/04_primitive_types/primitive_types5.rs b/exercises/04_primitive_types/primitive_types5.rs index f646986e..6e00ef51 100644 --- a/exercises/04_primitive_types/primitive_types5.rs +++ b/exercises/04_primitive_types/primitive_types5.rs @@ -1,15 +1,8 @@ -// primitive_types5.rs -// -// Destructure the `cat` tuple so that the println will work. -// -// Execute `rustlings hint primitive_types5` or use the `hint` watch subcommand -// for a hint. - -// I AM NOT DONE - fn main() { let cat = ("Furry McFurson", 3.5); - let /* your pattern here */ = cat; - println!("{} is {} years old.", name, age); + // TODO: Destructure the `cat` tuple in one statement so that the println works. + // let /* your pattern here */ = cat; + + println!("{name} is {age} years old"); } diff --git a/exercises/04_primitive_types/primitive_types6.rs b/exercises/04_primitive_types/primitive_types6.rs index 07cc46c6..a97e5311 100644 --- a/exercises/04_primitive_types/primitive_types6.rs +++ b/exercises/04_primitive_types/primitive_types6.rs @@ -1,19 +1,17 @@ -// primitive_types6.rs -// -// Use a tuple index to access the second element of `numbers`. You can put the -// expression for the second element where ??? is so that the test passes. -// -// Execute `rustlings hint primitive_types6` or use the `hint` watch subcommand -// for a hint. - -// I AM NOT DONE - -#[test] -fn indexing_tuple() { - let numbers = (1, 2, 3); - // Replace below ??? with the tuple indexing syntax. - let second = ???; - - assert_eq!(2, second, - "This is not the 2nd number in the tuple!") +fn main() { + // You can optionally experiment here. +} + +#[cfg(test)] +mod tests { + #[test] + fn indexing_tuple() { + let numbers = (1, 2, 3); + + // TODO: Use a tuple index to access the second element of `numbers` + // and assign it to a variable called `second`. + // let second = ???; + + assert_eq!(second, 2, "This is not the 2nd number in the tuple!"); + } } diff --git a/exercises/05_vecs/vecs1.rs b/exercises/05_vecs/vecs1.rs index 65b7a7f8..68e1affa 100644 --- a/exercises/05_vecs/vecs1.rs +++ b/exercises/05_vecs/vecs1.rs @@ -1,21 +1,17 @@ -// vecs1.rs -// -// Your task is to create a `Vec` which holds the exact same elements as in the -// array `a`. -// -// Make me compile and pass the test! -// -// Execute `rustlings hint vecs1` or use the `hint` watch subcommand for a hint. - -// I AM NOT DONE - fn array_and_vec() -> ([i32; 4], Vec) { - let a = [10, 20, 30, 40]; // a plain array - let v = // TODO: declare your vector here with the macro for vectors + let a = [10, 20, 30, 40]; // Array + + // TODO: Create a vector called `v` which contains the exact same elements as in the array `a`. + // Use the vector macro. + // let v = ???; (a, v) } +fn main() { + // You can optionally experiment here. +} + #[cfg(test)] mod tests { use super::*; @@ -23,6 +19,6 @@ mod tests { #[test] fn test_array_and_vec_similarity() { let (a, v) = array_and_vec(); - assert_eq!(a, v[..]); + assert_eq!(a, *v); } } diff --git a/exercises/05_vecs/vecs2.rs b/exercises/05_vecs/vecs2.rs index e92c970a..a9be2580 100644 --- a/exercises/05_vecs/vecs2.rs +++ b/exercises/05_vecs/vecs2.rs @@ -1,31 +1,36 @@ -// vecs2.rs -// -// A Vec of even numbers is given. Your task is to complete the loop so that -// each number in the Vec is multiplied by 2. -// -// Make me pass the test! -// -// Execute `rustlings hint vecs2` or use the `hint` watch subcommand for a hint. +fn vec_loop(input: &[i32]) -> Vec { + let mut output = Vec::new(); -// I AM NOT DONE - -fn vec_loop(mut v: Vec) -> Vec { - for element in v.iter_mut() { - // TODO: Fill this up so that each element in the Vec `v` is - // multiplied by 2. - ??? + for element in input { + // TODO: Multiply each element in the `input` slice by 2 and push it to + // the `output` vector. } - // At this point, `v` should be equal to [4, 8, 12, 16, 20]. - v + output } -fn vec_map(v: &Vec) -> Vec { - v.iter().map(|element| { - // TODO: Do the same thing as above - but instead of mutating the - // Vec, you can just return the new number! - ??? - }).collect() +fn vec_map_example(input: &[i32]) -> Vec { + // An example of collecting a vector after mapping. + // We map each element of the `input` slice to its value plus 1. + // If the input is `[1, 2, 3]`, the output is `[2, 3, 4]`. + input.iter().map(|element| element + 1).collect() +} + +fn vec_map(input: &[i32]) -> Vec { + // TODO: Here, we also want to multiply each element in the `input` slice + // by 2, but with iterator mapping instead of manually pushing into an empty + // vector. + // See the example in the function `vec_map_example` above. + input + .iter() + .map(|element| { + // ??? + }) + .collect() +} + +fn main() { + // You can optionally experiment here. } #[cfg(test)] @@ -34,17 +39,22 @@ mod tests { #[test] fn test_vec_loop() { - let v: Vec = (1..).filter(|x| x % 2 == 0).take(5).collect(); - let ans = vec_loop(v.clone()); + let input = [2, 4, 6, 8, 10]; + let ans = vec_loop(&input); + assert_eq!(ans, [4, 8, 12, 16, 20]); + } - assert_eq!(ans, v.iter().map(|x| x * 2).collect::>()); + #[test] + fn test_vec_map_example() { + let input = [1, 2, 3]; + let ans = vec_map_example(&input); + assert_eq!(ans, [2, 3, 4]); } #[test] fn test_vec_map() { - let v: Vec = (1..).filter(|x| x % 2 == 0).take(5).collect(); - let ans = vec_map(&v); - - assert_eq!(ans, v.iter().map(|x| x * 2).collect::>()); + let input = [2, 4, 6, 8, 10]; + let ans = vec_map(&input); + assert_eq!(ans, [4, 8, 12, 16, 20]); } } diff --git a/exercises/06_move_semantics/move_semantics1.rs b/exercises/06_move_semantics/move_semantics1.rs index e0639375..4eb3d618 100644 --- a/exercises/06_move_semantics/move_semantics1.rs +++ b/exercises/06_move_semantics/move_semantics1.rs @@ -1,19 +1,4 @@ -// move_semantics1.rs -// -// Execute `rustlings hint move_semantics1` or use the `hint` watch subcommand -// for a hint. - -// I AM NOT DONE - -#[test] -fn main() { - let vec0 = vec![22, 44, 66]; - - let vec1 = fill_vec(vec0); - - assert_eq!(vec1, vec![22, 44, 66, 88]); -} - +// TODO: Fix the compiler error in this function. fn fill_vec(vec: Vec) -> Vec { let vec = vec; @@ -21,3 +6,19 @@ fn fill_vec(vec: Vec) -> Vec { vec } + +fn main() { + // You can optionally experiment here. +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn move_semantics1() { + let vec0 = vec![22, 44, 66]; + let vec1 = fill_vec(vec0); + assert_eq!(vec1, vec![22, 44, 66, 88]); + } +} diff --git a/exercises/06_move_semantics/move_semantics2.rs b/exercises/06_move_semantics/move_semantics2.rs index dc58be50..a3ab7a0f 100644 --- a/exercises/06_move_semantics/move_semantics2.rs +++ b/exercises/06_move_semantics/move_semantics2.rs @@ -1,22 +1,3 @@ -// move_semantics2.rs -// -// Make the test pass by finding a way to keep both Vecs separate! -// -// Execute `rustlings hint move_semantics2` or use the `hint` watch subcommand -// for a hint. - -// I AM NOT DONE - -#[test] -fn main() { - let vec0 = vec![22, 44, 66]; - - let vec1 = fill_vec(vec0); - - assert_eq!(vec0, vec![22, 44, 66]); - assert_eq!(vec1, vec![22, 44, 66, 88]); -} - fn fill_vec(vec: Vec) -> Vec { let mut vec = vec; @@ -24,3 +5,24 @@ fn fill_vec(vec: Vec) -> Vec { vec } + +fn main() { + // You can optionally experiment here. +} + +#[cfg(test)] +mod tests { + use super::*; + + // TODO: Make both vectors `vec0` and `vec1` accessible at the same time to + // fix the compiler error in the test. + #[test] + fn move_semantics2() { + let vec0 = vec![22, 44, 66]; + + let vec1 = fill_vec(vec0); + + assert_eq!(vec0, [22, 44, 66]); + assert_eq!(vec1, [22, 44, 66, 88]); + } +} diff --git a/exercises/06_move_semantics/move_semantics3.rs b/exercises/06_move_semantics/move_semantics3.rs index 7152c716..11dbbbeb 100644 --- a/exercises/06_move_semantics/move_semantics3.rs +++ b/exercises/06_move_semantics/move_semantics3.rs @@ -1,24 +1,22 @@ -// move_semantics3.rs -// -// Make me compile without adding new lines -- just changing existing lines! (no -// lines with multiple semicolons necessary!) -// -// Execute `rustlings hint move_semantics3` or use the `hint` watch subcommand -// for a hint. - -// I AM NOT DONE - -#[test] -fn main() { - let vec0 = vec![22, 44, 66]; - - let vec1 = fill_vec(vec0); - - assert_eq!(vec1, vec![22, 44, 66, 88]); -} - +// TODO: Fix the compiler error in the function without adding any new line. fn fill_vec(vec: Vec) -> Vec { vec.push(88); vec } + +fn main() { + // You can optionally experiment here. +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn move_semantics3() { + let vec0 = vec![22, 44, 66]; + let vec1 = fill_vec(vec0); + assert_eq!(vec1, [22, 44, 66, 88]); + } +} diff --git a/exercises/06_move_semantics/move_semantics4.rs b/exercises/06_move_semantics/move_semantics4.rs index bfc917fa..c225f3ba 100644 --- a/exercises/06_move_semantics/move_semantics4.rs +++ b/exercises/06_move_semantics/move_semantics4.rs @@ -1,29 +1,18 @@ -// move_semantics4.rs -// -// Refactor this code so that instead of passing `vec0` into the `fill_vec` -// function, the Vector gets created in the function itself and passed back to -// the main function. -// -// Execute `rustlings hint move_semantics4` or use the `hint` watch subcommand -// for a hint. - -// I AM NOT DONE - -#[test] fn main() { - let vec0 = vec![22, 44, 66]; - - let vec1 = fill_vec(vec0); - - assert_eq!(vec1, vec![22, 44, 66, 88]); + // You can optionally experiment here. } -// `fill_vec()` no longer takes `vec: Vec` as argument - don't change this! -fn fill_vec() -> Vec { - // Instead, let's create and fill the Vec in here - how do you do that? - let mut vec = vec; - - vec.push(88); - - vec +#[cfg(test)] +mod tests { + // TODO: Fix the compiler errors only by reordering the lines in the test. + // Don't add, change or remove any line. + #[test] + fn move_semantics5() { + let mut x = 100; + let y = &mut x; + let z = &mut x; + *y += 100; + *z += 1000; + assert_eq!(x, 1200); + } } diff --git a/exercises/06_move_semantics/move_semantics5.rs b/exercises/06_move_semantics/move_semantics5.rs index 267bdccc..65065688 100644 --- a/exercises/06_move_semantics/move_semantics5.rs +++ b/exercises/06_move_semantics/move_semantics5.rs @@ -1,19 +1,22 @@ -// move_semantics5.rs -// -// Make me compile only by reordering the lines in `main()`, but without adding, -// changing or removing any of them. -// -// Execute `rustlings hint move_semantics5` or use the `hint` watch subcommand -// for a hint. +// TODO: Fix the compiler errors without changing anything except adding or +// removing references (the character `&`). -// I AM NOT DONE - -#[test] fn main() { - let mut x = 100; - let y = &mut x; - let z = &mut x; - *y += 100; - *z += 1000; - assert_eq!(x, 1200); + let data = "Rust is great!".to_string(); + + get_char(data); + + string_uppercase(&data); +} + +// Shouldn't take ownership +fn get_char(data: String) -> char { + data.chars().last().unwrap() +} + +// Should take ownership +fn string_uppercase(mut data: &String) { + data = &data.to_uppercase(); + + println!("{data}"); } diff --git a/exercises/06_move_semantics/move_semantics6.rs b/exercises/06_move_semantics/move_semantics6.rs deleted file mode 100644 index cace4ca6..00000000 --- a/exercises/06_move_semantics/move_semantics6.rs +++ /dev/null @@ -1,28 +0,0 @@ -// move_semantics6.rs -// -// You can't change anything except adding or removing references. -// -// Execute `rustlings hint move_semantics6` or use the `hint` watch subcommand -// for a hint. - -// I AM NOT DONE - -fn main() { - let data = "Rust is great!".to_string(); - - get_char(data); - - string_uppercase(&data); -} - -// Should not take ownership -fn get_char(data: String) -> char { - data.chars().last().unwrap() -} - -// Should take ownership -fn string_uppercase(mut data: &String) { - data = &data.to_uppercase(); - - println!("{}", data); -} diff --git a/exercises/07_structs/structs1.rs b/exercises/07_structs/structs1.rs index 5fa5821c..959c4c6a 100644 --- a/exercises/07_structs/structs1.rs +++ b/exercises/07_structs/structs1.rs @@ -1,28 +1,24 @@ -// structs1.rs -// -// Address all the TODOs to make the tests pass! -// -// Execute `rustlings hint structs1` or use the `hint` watch subcommand for a -// hint. - -// I AM NOT DONE - -struct ColorClassicStruct { - // TODO: Something goes here +struct ColorRegularStruct { + // TODO: Add the fields that the test `regular_structs` expects. + // What types should the fields have? What are the minimum and maximum values for RGB colors? } -struct ColorTupleStruct(/* TODO: Something goes here */); +struct ColorTupleStruct(/* TODO: Add the fields that the test `tuple_structs` expects */); #[derive(Debug)] -struct UnitLikeStruct; +struct UnitStruct; + +fn main() { + // You can optionally experiment here. +} #[cfg(test)] mod tests { use super::*; #[test] - fn classic_c_structs() { - // TODO: Instantiate a classic c struct! + fn regular_structs() { + // TODO: Instantiate a regular struct. // let green = assert_eq!(green.red, 0); @@ -32,7 +28,7 @@ mod tests { #[test] fn tuple_structs() { - // TODO: Instantiate a tuple struct! + // TODO: Instantiate a tuple struct. // let green = assert_eq!(green.0, 0); @@ -42,10 +38,10 @@ mod tests { #[test] fn unit_structs() { - // TODO: Instantiate a unit-like struct! - // let unit_like_struct = - let message = format!("{:?}s are fun!", unit_like_struct); + // TODO: Instantiate a unit struct. + // let unit_struct = + let message = format!("{unit_struct:?}s are fun!"); - assert_eq!(message, "UnitLikeStructs are fun!"); + assert_eq!(message, "UnitStructs are fun!"); } } diff --git a/exercises/07_structs/structs2.rs b/exercises/07_structs/structs2.rs index 328567f0..79141af9 100644 --- a/exercises/07_structs/structs2.rs +++ b/exercises/07_structs/structs2.rs @@ -1,12 +1,3 @@ -// structs2.rs -// -// Address all the TODOs to make the tests pass! -// -// Execute `rustlings hint structs2` or use the `hint` watch subcommand for a -// hint. - -// I AM NOT DONE - #[derive(Debug)] struct Order { name: String, @@ -30,6 +21,10 @@ fn create_order_template() -> Order { } } +fn main() { + // You can optionally experiment here. +} + #[cfg(test)] mod tests { use super::*; @@ -37,8 +32,10 @@ mod tests { #[test] fn your_order() { let order_template = create_order_template(); + // TODO: Create your own order using the update syntax and template above! // let your_order = + assert_eq!(your_order.name, "Hacker in Rust"); assert_eq!(your_order.year, order_template.year); assert_eq!(your_order.made_by_phone, order_template.made_by_phone); diff --git a/exercises/07_structs/structs3.rs b/exercises/07_structs/structs3.rs index 7cda5af1..93b57fef 100644 --- a/exercises/07_structs/structs3.rs +++ b/exercises/07_structs/structs3.rs @@ -1,13 +1,5 @@ -// structs3.rs -// // Structs contain data, but can also have logic. In this exercise we have -// defined the Package struct and we want to test some logic attached to it. -// Make the code compile and the tests pass! -// -// Execute `rustlings hint structs3` or use the `hint` watch subcommand for a -// hint. - -// I AM NOT DONE +// defined the `Package` struct and we want to test some logic attached to it. #[derive(Debug)] struct Package { @@ -17,29 +9,36 @@ struct Package { } impl Package { - fn new(sender_country: String, recipient_country: String, weight_in_grams: u32) -> Package { + fn new(sender_country: String, recipient_country: String, weight_in_grams: u32) -> Self { if weight_in_grams < 10 { - // This is not how you should handle errors in Rust, - // but we will learn about error handling later. - panic!("Can not ship a package with weight below 10 grams.") - } else { - Package { - sender_country, - recipient_country, - weight_in_grams, - } + // This isn't how you should handle errors in Rust, but we will + // learn about error handling later. + panic!("Can't ship a package with weight below 10 grams"); + } + + Self { + sender_country, + recipient_country, + weight_in_grams, } } - fn is_international(&self) -> ??? { - // Something goes here... + // TODO: Add the correct return type to the function signature. + fn is_international(&self) { + // TODO: Read the tests that use this method to find out when a package + // is considered international. } - fn get_fees(&self, cents_per_gram: u32) -> ??? { - // Something goes here... + // TODO: Add the correct return type to the function signature. + fn get_fees(&self, cents_per_gram: u32) { + // TODO: Calculate the package's fees. } } +fn main() { + // You can optionally experiment here. +} + #[cfg(test)] mod tests { use super::*; diff --git a/exercises/08_enums/enums1.rs b/exercises/08_enums/enums1.rs index 25525b25..99f70874 100644 --- a/exercises/08_enums/enums1.rs +++ b/exercises/08_enums/enums1.rs @@ -1,12 +1,6 @@ -// enums1.rs -// -// No hints this time! ;) - -// I AM NOT DONE - #[derive(Debug)] enum Message { - // TODO: define a few types of messages as used below + // TODO: Define a few types of messages as used below. } fn main() { diff --git a/exercises/08_enums/enums2.rs b/exercises/08_enums/enums2.rs index df93fe0f..14aa29ad 100644 --- a/exercises/08_enums/enums2.rs +++ b/exercises/08_enums/enums2.rs @@ -1,13 +1,6 @@ -// enums2.rs -// -// Execute `rustlings hint enums2` or use the `hint` watch subcommand for a -// hint. - -// I AM NOT DONE - #[derive(Debug)] enum Message { - // TODO: define the different variants used below + // TODO: Define the different variants used below. } impl Message { diff --git a/exercises/08_enums/enums3.rs b/exercises/08_enums/enums3.rs index 92d18c46..7dd21715 100644 --- a/exercises/08_enums/enums3.rs +++ b/exercises/08_enums/enums3.rs @@ -1,14 +1,5 @@ -// enums3.rs -// -// Address all the TODOs to make the tests pass! -// -// Execute `rustlings hint enums3` or use the `hint` watch subcommand for a -// hint. - -// I AM NOT DONE - enum Message { - // TODO: implement the message variant types based on their usage below + // TODO: Implement the message variant types based on their usage below. } struct Point { @@ -33,20 +24,24 @@ impl State { } fn echo(&mut self, s: String) { - self.message = s + self.message = s; } - fn move_position(&mut self, p: Point) { - self.position = p; + fn move_position(&mut self, point: Point) { + self.position = point; } fn process(&mut self, message: Message) { - // TODO: create a match expression to process the different message variants + // TODO: Create a match expression to process the different message variants. // Remember: When passing a tuple as a function argument, you'll need extra parentheses: - // fn function((t, u, p, l, e)) + // e.g. `foo((t, u, p, l, e))` } } +fn main() { + // You can optionally experiment here. +} + #[cfg(test)] mod tests { use super::*; @@ -57,8 +52,9 @@ mod tests { quit: false, position: Point { x: 0, y: 0 }, color: (0, 0, 0), - message: "hello world".to_string(), + message: String::from("hello world"), }; + state.process(Message::ChangeColor(255, 0, 255)); state.process(Message::Echo(String::from("Hello world!"))); state.process(Message::Move(Point { x: 10, y: 15 })); @@ -67,7 +63,7 @@ mod tests { assert_eq!(state.color, (255, 0, 255)); assert_eq!(state.position.x, 10); assert_eq!(state.position.y, 15); - assert_eq!(state.quit, true); + assert!(state.quit); assert_eq!(state.message, "Hello world!"); } } diff --git a/exercises/09_strings/strings1.rs b/exercises/09_strings/strings1.rs index f50e1fa9..6abdbb48 100644 --- a/exercises/09_strings/strings1.rs +++ b/exercises/09_strings/strings1.rs @@ -1,17 +1,9 @@ -// strings1.rs -// -// Make me compile without changing the function signature! -// -// Execute `rustlings hint strings1` or use the `hint` watch subcommand for a -// hint. - -// I AM NOT DONE - -fn main() { - let answer = current_favorite_color(); - println!("My current favorite color is {}", answer); -} - +// TODO: Fix the compiler error without changing the function signature. fn current_favorite_color() -> String { "blue" } + +fn main() { + let answer = current_favorite_color(); + println!("My current favorite color is {answer}"); +} diff --git a/exercises/09_strings/strings2.rs b/exercises/09_strings/strings2.rs index 4d95d16a..93d9cb6b 100644 --- a/exercises/09_strings/strings2.rs +++ b/exercises/09_strings/strings2.rs @@ -1,21 +1,14 @@ -// strings2.rs -// -// Make me compile without changing the function signature! -// -// Execute `rustlings hint strings2` or use the `hint` watch subcommand for a -// hint. - -// I AM NOT DONE +// TODO: Fix the compiler error in the `main` function without changing this function. +fn is_a_color_word(attempt: &str) -> bool { + attempt == "green" || attempt == "blue" || attempt == "red" +} fn main() { - let word = String::from("green"); // Try not changing this line :) + let word = String::from("green"); // Don't change this line. + if is_a_color_word(word) { println!("That is a color word I know!"); } else { println!("That is not a color word I know."); } } - -fn is_a_color_word(attempt: &str) -> bool { - attempt == "green" || attempt == "blue" || attempt == "red" -} diff --git a/exercises/09_strings/strings3.rs b/exercises/09_strings/strings3.rs index 384e7ce3..39fce18c 100644 --- a/exercises/09_strings/strings3.rs +++ b/exercises/09_strings/strings3.rs @@ -1,23 +1,17 @@ -// strings3.rs -// -// Execute `rustlings hint strings3` or use the `hint` watch subcommand for a -// hint. - -// I AM NOT DONE - -fn trim_me(input: &str) -> String { - // TODO: Remove whitespace from both ends of a string! - ??? +fn trim_me(input: &str) -> &str { + // TODO: Remove whitespace from both ends of a string. } fn compose_me(input: &str) -> String { - // TODO: Add " world!" to the string! There are multiple ways to do this! - ??? + // TODO: Add " world!" to the string! There are multiple ways to do this. } fn replace_me(input: &str) -> String { - // TODO: Replace "cars" in the string with "balloons"! - ??? + // TODO: Replace "cars" in the string with "balloons". +} + +fn main() { + // You can optionally experiment here. } #[cfg(test)] @@ -39,7 +33,13 @@ mod tests { #[test] fn replace_a_string() { - assert_eq!(replace_me("I think cars are cool"), "I think balloons are cool"); - assert_eq!(replace_me("I love to look at cars"), "I love to look at balloons"); + assert_eq!( + replace_me("I think cars are cool"), + "I think balloons are cool", + ); + assert_eq!( + replace_me("I love to look at cars"), + "I love to look at balloons", + ); } } diff --git a/exercises/09_strings/strings4.rs b/exercises/09_strings/strings4.rs index e8c54acc..9d9eb480 100644 --- a/exercises/09_strings/strings4.rs +++ b/exercises/09_strings/strings4.rs @@ -1,30 +1,36 @@ -// strings4.rs -// -// Ok, here are a bunch of values-- some are `String`s, some are `&str`s. Your -// task is to call one of these two functions on each value depending on what -// you think each value is. That is, add either `string_slice` or `string` -// before the parentheses on each line. If you're right, it will compile! -// -// No hints this time! - -// I AM NOT DONE +// Calls of this function should be replaced with calls of `string_slice` or `string`. +fn placeholder() {} fn string_slice(arg: &str) { - println!("{}", arg); + println!("{arg}"); } fn string(arg: String) { - println!("{}", arg); + println!("{arg}"); } +// TODO: Here are a bunch of values - some are `String`, some are `&str`. +// Your task is to replace `placeholder(…)` with either `string_slice(…)` +// or `string(…)` depending on what you think each value is. fn main() { - ???("blue"); - ???("red".to_string()); - ???(String::from("hi")); - ???("rust is fun!".to_owned()); - ???("nice weather".into()); - ???(format!("Interpolation {}", "Station")); - ???(&String::from("abc")[0..1]); - ???(" hello there ".trim()); - ???("Happy Monday!".to_string().replace("Mon", "Tues")); - ???("mY sHiFt KeY iS sTiCkY".to_lowercase()); + placeholder("blue"); + + placeholder("red".to_string()); + + placeholder(String::from("hi")); + + placeholder("rust is fun!".to_owned()); + + placeholder("nice weather".into()); + + placeholder(format!("Interpolation {}", "Station")); + + // WARNING: This is byte indexing, not character indexing. + // Character indexing can be done using `s.chars().nth(INDEX)`. + placeholder(&String::from("abc")[0..1]); + + placeholder(" hello there ".trim()); + + placeholder("Happy Monday!".replace("Mon", "Tues")); + + placeholder("mY sHiFt KeY iS sTiCkY".to_lowercase()); } diff --git a/exercises/10_modules/modules1.rs b/exercises/10_modules/modules1.rs index 9eb5a48b..d97ab23a 100644 --- a/exercises/10_modules/modules1.rs +++ b/exercises/10_modules/modules1.rs @@ -1,10 +1,4 @@ -// modules1.rs -// -// Execute `rustlings hint modules1` or use the `hint` watch subcommand for a -// hint. - -// I AM NOT DONE - +// TODO: Fix the compiler error about calling a private function. mod sausage_factory { // Don't let anybody outside of this module see this! fn get_secret_recipe() -> String { diff --git a/exercises/10_modules/modules2.rs b/exercises/10_modules/modules2.rs index 04154543..782a70ea 100644 --- a/exercises/10_modules/modules2.rs +++ b/exercises/10_modules/modules2.rs @@ -1,27 +1,19 @@ -// modules2.rs -// // You can bring module paths into scopes and provide new names for them with -// the 'use' and 'as' keywords. Fix these 'use' statements to make the code -// compile. -// -// Execute `rustlings hint modules2` or use the `hint` watch subcommand for a -// hint. - -// I AM NOT DONE +// the `use` and `as` keywords. mod delicious_snacks { - // TODO: Fix these use statements - use self::fruits::PEAR as ??? - use self::veggies::CUCUMBER as ??? + // TODO: Add the following two `use` statements after fixing them. + // use self::fruits::PEAR as ???; + // use self::veggies::CUCUMBER as ???; mod fruits { - pub const PEAR: &'static str = "Pear"; - pub const APPLE: &'static str = "Apple"; + pub const PEAR: &str = "Pear"; + pub const APPLE: &str = "Apple"; } mod veggies { - pub const CUCUMBER: &'static str = "Cucumber"; - pub const CARROT: &'static str = "Carrot"; + pub const CUCUMBER: &str = "Cucumber"; + pub const CARROT: &str = "Carrot"; } } @@ -29,6 +21,6 @@ fn main() { println!( "favorite snacks: {} and {}", delicious_snacks::fruit, - delicious_snacks::veggie + delicious_snacks::veggie, ); } diff --git a/exercises/10_modules/modules3.rs b/exercises/10_modules/modules3.rs index f2bb0503..691608d2 100644 --- a/exercises/10_modules/modules3.rs +++ b/exercises/10_modules/modules3.rs @@ -1,17 +1,9 @@ -// modules3.rs -// -// You can use the 'use' keyword to bring module paths from modules from -// anywhere and especially from the Rust standard library into your scope. Bring -// SystemTime and UNIX_EPOCH from the std::time module. Bonus style points if -// you can do it with one line! -// -// Execute `rustlings hint modules3` or use the `hint` watch subcommand for a -// hint. +// You can use the `use` keyword to bring module paths from modules from +// anywhere and especially from the standard library into your scope. -// I AM NOT DONE - -// TODO: Complete this use statement -use ??? +// TODO: Bring `SystemTime` and `UNIX_EPOCH` from the `std::time` module into +// your scope. Bonus style points if you can do it with one line! +// use ???; fn main() { match SystemTime::now().duration_since(UNIX_EPOCH) { diff --git a/exercises/11_hashmaps/hashmaps1.rs b/exercises/11_hashmaps/hashmaps1.rs index 02f4725e..74001d04 100644 --- a/exercises/11_hashmaps/hashmaps1.rs +++ b/exercises/11_hashmaps/hashmaps1.rs @@ -1,31 +1,27 @@ -// hashmaps1.rs -// // A basket of fruits in the form of a hash map needs to be defined. The key // represents the name of the fruit and the value represents how many of that -// particular fruit is in the basket. You have to put at least three different +// particular fruit is in the basket. You have to put at least 3 different // types of fruits (e.g. apple, banana, mango) in the basket and the total count -// of all the fruits should be at least five. -// -// Make me compile and pass the tests! -// -// Execute `rustlings hint hashmaps1` or use the `hint` watch subcommand for a -// hint. - -// I AM NOT DONE +// of all the fruits should be at least 5. use std::collections::HashMap; fn fruit_basket() -> HashMap { - let mut basket = // TODO: declare your hash map here. + // TODO: Declare the hash map. + // let mut basket = // Two bananas are already given for you :) basket.insert(String::from("banana"), 2); - // TODO: Put more fruits in your basket here. + // TODO: Put more fruits in your basket. basket } +fn main() { + // You can optionally experiment here. +} + #[cfg(test)] mod tests { use super::*; diff --git a/exercises/11_hashmaps/hashmaps2.rs b/exercises/11_hashmaps/hashmaps2.rs index a5925690..b3691b68 100644 --- a/exercises/11_hashmaps/hashmaps2.rs +++ b/exercises/11_hashmaps/hashmaps2.rs @@ -1,5 +1,3 @@ -// hashmaps2.rs -// // We're collecting different fruits to bake a delicious fruit cake. For this, // we have a basket, which we'll represent in the form of a hash map. The key // represents the name of each fruit we collect and the value represents how @@ -8,17 +6,10 @@ // must add fruit to the basket so that there is at least one of each kind and // more than 11 in total - we have a lot of mouths to feed. You are not allowed // to insert any more of these fruits! -// -// Make me pass the tests! -// -// Execute `rustlings hint hashmaps2` or use the `hint` watch subcommand for a -// hint. - -// I AM NOT DONE use std::collections::HashMap; -#[derive(Hash, PartialEq, Eq)] +#[derive(Hash, PartialEq, Eq, Debug)] enum Fruit { Apple, Banana, @@ -28,7 +19,7 @@ enum Fruit { } fn fruit_basket(basket: &mut HashMap) { - let fruit_kinds = vec![ + let fruit_kinds = [ Fruit::Apple, Fruit::Banana, Fruit::Mango, @@ -43,18 +34,18 @@ fn fruit_basket(basket: &mut HashMap) { } } +fn main() { + // You can optionally experiment here. +} + #[cfg(test)] mod tests { use super::*; // Don't modify this function! fn get_fruit_basket() -> HashMap { - let mut basket = HashMap::::new(); - basket.insert(Fruit::Apple, 4); - basket.insert(Fruit::Mango, 2); - basket.insert(Fruit::Lychee, 5); - - basket + let content = [(Fruit::Apple, 4), (Fruit::Mango, 2), (Fruit::Lychee, 5)]; + HashMap::from_iter(content) } #[test] @@ -81,13 +72,25 @@ mod tests { let count = basket.values().sum::(); assert!(count > 11); } - + #[test] fn all_fruit_types_in_basket() { + let fruit_kinds = [ + Fruit::Apple, + Fruit::Banana, + Fruit::Mango, + Fruit::Lychee, + Fruit::Pineapple, + ]; + let mut basket = get_fruit_basket(); fruit_basket(&mut basket); - for amount in basket.values() { - assert_ne!(amount, &0); + + for fruit_kind in fruit_kinds { + let Some(amount) = basket.get(&fruit_kind) else { + panic!("Fruit kind {fruit_kind:?} was not found in basket"); + }; + assert!(*amount > 0); } } } diff --git a/exercises/11_hashmaps/hashmaps3.rs b/exercises/11_hashmaps/hashmaps3.rs index 8d9236df..9f8fdd78 100644 --- a/exercises/11_hashmaps/hashmaps3.rs +++ b/exercises/11_hashmaps/hashmaps3.rs @@ -1,87 +1,77 @@ -// hashmaps3.rs -// // A list of scores (one per line) of a soccer match is given. Each line is of -// the form : ",,," -// Example: England,France,4,2 (England scored 4 goals, France 2). +// the form ",,," +// Example: "England,France,4,2" (England scored 4 goals, France 2). // // You have to build a scores table containing the name of the team, the total -// number of goals the team scored, and the total number of goals the team -// conceded. One approach to build the scores table is to use a Hashmap. -// The solution is partially written to use a Hashmap, -// complete it to pass the test. -// -// Make me pass the tests! -// -// Execute `rustlings hint hashmaps3` or use the `hint` watch subcommand for a -// hint. - -// I AM NOT DONE +// number of goals the team scored, and the total number of goals the team +// conceded. use std::collections::HashMap; // A structure to store the goal details of a team. +#[derive(Default)] struct Team { goals_scored: u8, goals_conceded: u8, } -fn build_scores_table(results: String) -> HashMap { +fn build_scores_table(results: &str) -> HashMap<&str, Team> { // The name of the team is the key and its associated struct is the value. - let mut scores: HashMap = HashMap::new(); + let mut scores = HashMap::new(); - for r in results.lines() { - let v: Vec<&str> = r.split(',').collect(); - let team_1_name = v[0].to_string(); - let team_1_score: u8 = v[2].parse().unwrap(); - let team_2_name = v[1].to_string(); - let team_2_score: u8 = v[3].parse().unwrap(); - // TODO: Populate the scores table with details extracted from the - // current line. Keep in mind that goals scored by team_1 - // will be the number of goals conceded by team_2, and similarly - // goals scored by team_2 will be the number of goals conceded by - // team_1. + for line in results.lines() { + let mut split_iterator = line.split(','); + // NOTE: We use `unwrap` because we didn't deal with error handling yet. + let team_1_name = split_iterator.next().unwrap(); + let team_2_name = split_iterator.next().unwrap(); + let team_1_score: u8 = split_iterator.next().unwrap().parse().unwrap(); + let team_2_score: u8 = split_iterator.next().unwrap().parse().unwrap(); + + // TODO: Populate the scores table with the extracted details. + // Keep in mind that goals scored by team 1 will be the number of goals + // conceded by team 2. Similarly, goals scored by team 2 will be the + // number of goals conceded by team 1. } + scores } +fn main() { + // You can optionally experiment here. +} + #[cfg(test)] mod tests { use super::*; - fn get_results() -> String { - let results = "".to_string() - + "England,France,4,2\n" - + "France,Italy,3,1\n" - + "Poland,Spain,2,0\n" - + "Germany,England,2,1\n"; - results - } + const RESULTS: &str = "England,France,4,2 +France,Italy,3,1 +Poland,Spain,2,0 +Germany,England,2,1 +England,Spain,1,0"; #[test] fn build_scores() { - let scores = build_scores_table(get_results()); + let scores = build_scores_table(RESULTS); - let mut keys: Vec<&String> = scores.keys().collect(); - keys.sort(); - assert_eq!( - keys, - vec!["England", "France", "Germany", "Italy", "Poland", "Spain"] - ); + assert!(["England", "France", "Germany", "Italy", "Poland", "Spain"] + .into_iter() + .all(|team_name| scores.contains_key(team_name))); } #[test] fn validate_team_score_1() { - let scores = build_scores_table(get_results()); + let scores = build_scores_table(RESULTS); let team = scores.get("England").unwrap(); - assert_eq!(team.goals_scored, 5); + assert_eq!(team.goals_scored, 6); assert_eq!(team.goals_conceded, 4); } #[test] fn validate_team_score_2() { - let scores = build_scores_table(get_results()); + let scores = build_scores_table(RESULTS); let team = scores.get("Spain").unwrap(); assert_eq!(team.goals_scored, 0); - assert_eq!(team.goals_conceded, 2); + assert_eq!(team.goals_conceded, 3); } } diff --git a/exercises/12_options/options1.rs b/exercises/12_options/options1.rs index 3cbfecd6..5009f8b6 100644 --- a/exercises/12_options/options1.rs +++ b/exercises/12_options/options1.rs @@ -1,25 +1,27 @@ -// options1.rs -// -// Execute `rustlings hint options1` or use the `hint` watch subcommand for a -// hint. - -// I AM NOT DONE - // This function returns how much icecream there is left in the fridge. -// If it's before 10PM, there's 5 scoops left. At 10PM, someone eats it -// all, so there'll be no more left :( -fn maybe_icecream(time_of_day: u16) -> Option { - // We use the 24-hour system here, so 10PM is a value of 22 and 12AM is a - // value of 0. The Option output should gracefully handle cases where - // time_of_day > 23. - // TODO: Complete the function body - remember to return an Option! - ??? +// If it's before 22:00 (24-hour system), then 5 scoops are left. At 22:00, +// someone eats it all, so no icecream is left (value 0). Return `None` if +// `hour_of_day` is higher than 23. +fn maybe_icecream(hour_of_day: u16) -> Option { + // TODO: Complete the function body. +} + +fn main() { + // You can optionally experiment here. } #[cfg(test)] mod tests { use super::*; + #[test] + fn raw_value() { + // TODO: Fix this test. How do you get the value contained in the + // Option? + let icecreams = maybe_icecream(12); + assert_eq!(icecreams, 5); + } + #[test] fn check_icecream() { assert_eq!(maybe_icecream(0), Some(5)); @@ -27,14 +29,7 @@ mod tests { assert_eq!(maybe_icecream(18), Some(5)); assert_eq!(maybe_icecream(22), Some(0)); assert_eq!(maybe_icecream(23), Some(0)); + assert_eq!(maybe_icecream(24), None); assert_eq!(maybe_icecream(25), None); } - - #[test] - fn raw_value() { - // TODO: Fix this test. How do you get at the value contained in the - // Option? - let icecreams = maybe_icecream(12); - assert_eq!(icecreams, 5); - } } diff --git a/exercises/12_options/options2.rs b/exercises/12_options/options2.rs index 4d998e7d..07c27c6e 100644 --- a/exercises/12_options/options2.rs +++ b/exercises/12_options/options2.rs @@ -1,9 +1,6 @@ -// options2.rs -// -// Execute `rustlings hint options2` or use the `hint` watch subcommand for a -// hint. - -// I AM NOT DONE +fn main() { + // You can optionally experiment here. +} #[cfg(test)] mod tests { @@ -12,7 +9,7 @@ mod tests { let target = "rustlings"; let optional_target = Some(target); - // TODO: Make this an if let statement whose value is "Some" type + // TODO: Make this an if-let statement whose value is `Some`. word = optional_target { assert_eq!(word, target); } @@ -23,15 +20,15 @@ mod tests { let range = 10; let mut optional_integers: Vec> = vec![None]; - for i in 1..(range + 1) { + for i in 1..=range { optional_integers.push(Some(i)); } let mut cursor = range; - // TODO: make this a while let statement - remember that vector.pop also - // adds another layer of Option. You can stack `Option`s into - // while let and if let. + // TODO: Make this a while-let statement. Remember that `Vec::pop()` + // adds another layer of `Option`. You can do nested pattern matching + // in if-let and while-let statements. integer = optional_integers.pop() { assert_eq!(integer, cursor); cursor -= 1; diff --git a/exercises/12_options/options3.rs b/exercises/12_options/options3.rs index 23c15eab..4cedb512 100644 --- a/exercises/12_options/options3.rs +++ b/exercises/12_options/options3.rs @@ -1,21 +1,17 @@ -// options3.rs -// -// Execute `rustlings hint options3` or use the `hint` watch subcommand for a -// hint. - -// I AM NOT DONE - +#[derive(Debug)] struct Point { x: i32, y: i32, } fn main() { - let y: Option = Some(Point { x: 100, y: 200 }); + let optional_point = Some(Point { x: 100, y: 200 }); - match y { - Some(p) => println!("Co-ordinates are {},{} ", p.x, p.y), - _ => panic!("no match!"), + // TODO: Fix the compiler error by adding something to this match statement. + match optional_point { + Some(p) => println!("Co-ordinates are {},{}", p.x, p.y), + _ => panic!("No match!"), } - y; // Fix without deleting this line. + + println!("{optional_point:?}"); // Don't change this line. } diff --git a/exercises/13_error_handling/errors1.rs b/exercises/13_error_handling/errors1.rs index 0ba59a57..ec7cb3cb 100644 --- a/exercises/13_error_handling/errors1.rs +++ b/exercises/13_error_handling/errors1.rs @@ -1,25 +1,22 @@ -// errors1.rs -// -// This function refuses to generate text to be printed on a nametag if you pass -// it an empty string. It'd be nicer if it explained what the problem was, -// instead of just sometimes returning `None`. Thankfully, Rust has a similar -// construct to `Option` that can be used to express error conditions. Let's use -// it! -// -// Execute `rustlings hint errors1` or use the `hint` watch subcommand for a -// hint. - -// I AM NOT DONE - -pub fn generate_nametag_text(name: String) -> Option { +// TODO: This function refuses to generate text to be printed on a nametag if +// you pass it an empty string. It'd be nicer if it explained what the problem +// was instead of just returning `None`. Thankfully, Rust has a similar +// construct to `Option` that can be used to express error conditions. Change +// the function signature and body to return `Result` instead +// of `Option`. +fn generate_nametag_text(name: String) -> Option { if name.is_empty() { // Empty names aren't allowed. None } else { - Some(format!("Hi! My name is {}", name)) + Some(format!("Hi! My name is {name}")) } } +fn main() { + // You can optionally experiment here. +} + #[cfg(test)] mod tests { use super::*; @@ -27,17 +24,18 @@ mod tests { #[test] fn generates_nametag_text_for_a_nonempty_name() { assert_eq!( - generate_nametag_text("Beyoncé".into()), - Ok("Hi! My name is Beyoncé".into()) + generate_nametag_text("Beyoncé".to_string()).as_deref(), + Ok("Hi! My name is Beyoncé"), ); } #[test] fn explains_why_generating_nametag_text_fails() { assert_eq!( - generate_nametag_text("".into()), - // Don't change this line - Err("`name` was empty; it must be nonempty.".into()) + generate_nametag_text(String::new()) + .as_ref() + .map_err(|e| e.as_str()), + Err("Empty names aren't allowed"), ); } } diff --git a/exercises/13_error_handling/errors2.rs b/exercises/13_error_handling/errors2.rs index 631fe67f..e50a9299 100644 --- a/exercises/13_error_handling/errors2.rs +++ b/exercises/13_error_handling/errors2.rs @@ -1,39 +1,39 @@ -// errors2.rs -// // Say we're writing a game where you can buy items with tokens. All items cost // 5 tokens, and whenever you purchase items there is a processing fee of 1 // token. A player of the game will type in how many items they want to buy, and // the `total_cost` function will calculate the total cost of the items. Since -// the player typed in the quantity, though, we get it as a string-- and they -// might have typed anything, not just numbers! +// the player typed in the quantity, we get it as a string. They might have +// typed anything, not just numbers! // // Right now, this function isn't handling the error case at all (and isn't -// handling the success case properly either). What we want to do is: if we call +// handling the success case properly either). What we want to do is: If we call // the `total_cost` function on a string that is not a number, that function -// will return a `ParseIntError`, and in that case, we want to immediately -// return that error from our function and not try to multiply and add. +// will return a `ParseIntError`. In that case, we want to immediately return +// that error from our function and not try to multiply and add. // -// There are at least two ways to implement this that are both correct-- but one +// There are at least two ways to implement this that are both correct. But one // is a lot shorter! -// -// Execute `rustlings hint errors2` or use the `hint` watch subcommand for a -// hint. - -// I AM NOT DONE use std::num::ParseIntError; -pub fn total_cost(item_quantity: &str) -> Result { +fn total_cost(item_quantity: &str) -> Result { let processing_fee = 1; let cost_per_item = 5; + + // TODO: Handle the error case as described above. let qty = item_quantity.parse::(); Ok(qty * cost_per_item + processing_fee) } +fn main() { + // You can optionally experiment here. +} + #[cfg(test)] mod tests { use super::*; + use std::num::IntErrorKind; #[test] fn item_quantity_is_a_valid_number() { @@ -43,8 +43,8 @@ mod tests { #[test] fn item_quantity_is_an_invalid_number() { assert_eq!( - total_cost("beep boop").unwrap_err().to_string(), - "invalid digit found in string" + total_cost("beep boop").unwrap_err().kind(), + &IntErrorKind::InvalidDigit, ); } } diff --git a/exercises/13_error_handling/errors3.rs b/exercises/13_error_handling/errors3.rs index d42d3b17..33a7b877 100644 --- a/exercises/13_error_handling/errors3.rs +++ b/exercises/13_error_handling/errors3.rs @@ -1,16 +1,20 @@ -// errors3.rs -// // This is a program that is trying to use a completed version of the // `total_cost` function from the previous exercise. It's not working though! // Why not? What should we do to fix it? -// -// Execute `rustlings hint errors3` or use the `hint` watch subcommand for a -// hint. - -// I AM NOT DONE use std::num::ParseIntError; +// Don't change this function. +fn total_cost(item_quantity: &str) -> Result { + let processing_fee = 1; + let cost_per_item = 5; + let qty = item_quantity.parse::()?; + + Ok(qty * cost_per_item + processing_fee) +} + +// TODO: Fix the compiler error by changing the signature and body of the +// `main` function. fn main() { let mut tokens = 100; let pretend_user_input = "8"; @@ -21,14 +25,6 @@ fn main() { println!("You can't afford that many!"); } else { tokens -= cost; - println!("You now have {} tokens.", tokens); + println!("You now have {tokens} tokens."); } } - -pub fn total_cost(item_quantity: &str) -> Result { - let processing_fee = 1; - let cost_per_item = 5; - let qty = item_quantity.parse::()?; - - Ok(qty * cost_per_item + processing_fee) -} diff --git a/exercises/13_error_handling/errors4.rs b/exercises/13_error_handling/errors4.rs index d6d6fcb6..ba01e54b 100644 --- a/exercises/13_error_handling/errors4.rs +++ b/exercises/13_error_handling/errors4.rs @@ -1,32 +1,37 @@ -// errors4.rs -// -// Execute `rustlings hint errors4` or use the `hint` watch subcommand for a -// hint. - -// I AM NOT DONE - -#[derive(PartialEq, Debug)] -struct PositiveNonzeroInteger(u64); - #[derive(PartialEq, Debug)] enum CreationError { Negative, Zero, } +#[derive(PartialEq, Debug)] +struct PositiveNonzeroInteger(u64); + impl PositiveNonzeroInteger { - fn new(value: i64) -> Result { - // Hmm... Why is this always returning an Ok value? - Ok(PositiveNonzeroInteger(value as u64)) + fn new(value: i64) -> Result { + // TODO: This function shouldn't always return an `Ok`. + Ok(Self(value as u64)) } } -#[test] -fn test_creation() { - assert!(PositiveNonzeroInteger::new(10).is_ok()); - assert_eq!( - Err(CreationError::Negative), - PositiveNonzeroInteger::new(-10) - ); - assert_eq!(Err(CreationError::Zero), PositiveNonzeroInteger::new(0)); +fn main() { + // You can optionally experiment here. +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_creation() { + assert_eq!( + PositiveNonzeroInteger::new(10), + Ok(PositiveNonzeroInteger(10)), + ); + assert_eq!( + PositiveNonzeroInteger::new(-10), + Err(CreationError::Negative), + ); + assert_eq!(PositiveNonzeroInteger::new(0), Err(CreationError::Zero)); + } } diff --git a/exercises/13_error_handling/errors5.rs b/exercises/13_error_handling/errors5.rs index 92461a7e..d0044db2 100644 --- a/exercises/13_error_handling/errors5.rs +++ b/exercises/13_error_handling/errors5.rs @@ -1,45 +1,18 @@ -// errors5.rs -// -// This program uses an altered version of the code from errors4. -// -// This exercise uses some concepts that we won't get to until later in the -// course, like `Box` and the `From` trait. It's not important to understand -// them in detail right now, but you can read ahead if you like. For now, think -// of the `Box` type as an "I want anything that does ???" type, which, -// given Rust's usual standards for runtime safety, should strike you as -// somewhat lenient! +// This exercise is an altered version of the `errors4` exercise. It uses some +// concepts that we won't get to until later in the course, like `Box` and the +// `From` trait. It's not important to understand them in detail right now, but +// you can read ahead if you like. For now, think of the `Box` type as +// an "I want anything that does ???" type. // // In short, this particular use case for boxes is for when you want to own a // value and you care only that it is a type which implements a particular -// trait. To do so, The Box is declared as of type Box where Trait is -// the trait the compiler looks for on any value used in that context. For this -// exercise, that context is the potential errors which can be returned in a -// Result. -// -// What can we use to describe both errors? In other words, is there a trait -// which both errors implement? -// -// Execute `rustlings hint errors5` or use the `hint` watch subcommand for a -// hint. +// trait. To do so, The `Box` is declared as of type `Box` where +// `Trait` is the trait the compiler looks for on any value used in that +// context. For this exercise, that context is the potential errors which +// can be returned in a `Result`. -// I AM NOT DONE - -use std::error; +use std::error::Error; use std::fmt; -use std::num::ParseIntError; - -// TODO: update the return type of `main()` to make this compile. -fn main() -> Result<(), Box> { - let pretend_user_input = "42"; - let x: i64 = pretend_user_input.parse()?; - println!("output={:?}", PositiveNonzeroInteger::new(x)?); - Ok(()) -} - -// Don't change anything below this line. - -#[derive(PartialEq, Debug)] -struct PositiveNonzeroInteger(u64); #[derive(PartialEq, Debug)] enum CreationError { @@ -47,17 +20,7 @@ enum CreationError { Zero, } -impl PositiveNonzeroInteger { - fn new(value: i64) -> Result { - match value { - x if x < 0 => Err(CreationError::Negative), - x if x == 0 => Err(CreationError::Zero), - x => Ok(PositiveNonzeroInteger(x as u64)), - } - } -} - -// This is required so that `CreationError` can implement `error::Error`. +// This is required so that `CreationError` can implement `Error`. impl fmt::Display for CreationError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { let description = match *self { @@ -68,4 +31,26 @@ impl fmt::Display for CreationError { } } -impl error::Error for CreationError {} +impl Error for CreationError {} + +#[derive(PartialEq, Debug)] +struct PositiveNonzeroInteger(u64); + +impl PositiveNonzeroInteger { + fn new(value: i64) -> Result { + match value { + 0 => Err(CreationError::Zero), + x if x < 0 => Err(CreationError::Negative), + x => Ok(PositiveNonzeroInteger(x as u64)), + } + } +} + +// TODO: Add the correct return type `Result<(), Box>`. What can we +// use to describe both errors? Is there a trait which both errors implement? +fn main() { + let pretend_user_input = "42"; + let x: i64 = pretend_user_input.parse()?; + println!("output={:?}", PositiveNonzeroInteger::new(x)?); + Ok(()) +} diff --git a/exercises/13_error_handling/errors6.rs b/exercises/13_error_handling/errors6.rs index aaf0948e..0652abda 100644 --- a/exercises/13_error_handling/errors6.rs +++ b/exercises/13_error_handling/errors6.rs @@ -1,94 +1,90 @@ -// errors6.rs -// -// Using catch-all error types like `Box` isn't recommended -// for library code, where callers might want to make decisions based on the -// error content, instead of printing it out or propagating it further. Here, we -// define a custom error type to make it possible for callers to decide what to -// do next when our function returns an error. -// -// Execute `rustlings hint errors6` or use the `hint` watch subcommand for a -// hint. - -// I AM NOT DONE +// Using catch-all error types like `Box` isn't recommended for +// library code where callers might want to make decisions based on the error +// content instead of printing it out or propagating it further. Here, we define +// a custom error type to make it possible for callers to decide what to do next +// when our function returns an error. use std::num::ParseIntError; -// This is a custom error type that we will be using in `parse_pos_nonzero()`. -#[derive(PartialEq, Debug)] -enum ParsePosNonzeroError { - Creation(CreationError), - ParseInt(ParseIntError), -} - -impl ParsePosNonzeroError { - fn from_creation(err: CreationError) -> ParsePosNonzeroError { - ParsePosNonzeroError::Creation(err) - } - // TODO: add another error conversion function here. - // fn from_parseint... -} - -fn parse_pos_nonzero(s: &str) -> Result { - // TODO: change this to return an appropriate error instead of panicking - // when `parse()` returns an error. - let x: i64 = s.parse().unwrap(); - PositiveNonzeroInteger::new(x).map_err(ParsePosNonzeroError::from_creation) -} - -// Don't change anything below this line. - -#[derive(PartialEq, Debug)] -struct PositiveNonzeroInteger(u64); - #[derive(PartialEq, Debug)] enum CreationError { Negative, Zero, } +// A custom error type that we will be using in `PositiveNonzeroInteger::parse`. +#[derive(PartialEq, Debug)] +enum ParsePosNonzeroError { + Creation(CreationError), + ParseInt(ParseIntError), +} + +impl ParsePosNonzeroError { + fn from_creation(err: CreationError) -> Self { + Self::Creation(err) + } + + // TODO: Add another error conversion function here. + // fn from_parseint(???) -> Self { ??? } +} + +#[derive(PartialEq, Debug)] +struct PositiveNonzeroInteger(u64); + impl PositiveNonzeroInteger { - fn new(value: i64) -> Result { + fn new(value: i64) -> Result { match value { x if x < 0 => Err(CreationError::Negative), x if x == 0 => Err(CreationError::Zero), - x => Ok(PositiveNonzeroInteger(x as u64)), + x => Ok(Self(x as u64)), } } + + fn parse(s: &str) -> Result { + // TODO: change this to return an appropriate error instead of panicking + // when `parse()` returns an error. + let x: i64 = s.parse().unwrap(); + Self::new(x).map_err(ParsePosNonzeroError::from_creation) + } +} + +fn main() { + // You can optionally experiment here. } #[cfg(test)] mod test { use super::*; + use std::num::IntErrorKind; #[test] fn test_parse_error() { - // We can't construct a ParseIntError, so we have to pattern match. assert!(matches!( - parse_pos_nonzero("not a number"), - Err(ParsePosNonzeroError::ParseInt(_)) + PositiveNonzeroInteger::parse("not a number"), + Err(ParsePosNonzeroError::ParseInt(_)), )); } #[test] fn test_negative() { assert_eq!( - parse_pos_nonzero("-555"), - Err(ParsePosNonzeroError::Creation(CreationError::Negative)) + PositiveNonzeroInteger::parse("-555"), + Err(ParsePosNonzeroError::Creation(CreationError::Negative)), ); } #[test] fn test_zero() { assert_eq!( - parse_pos_nonzero("0"), - Err(ParsePosNonzeroError::Creation(CreationError::Zero)) + PositiveNonzeroInteger::parse("0"), + Err(ParsePosNonzeroError::Creation(CreationError::Zero)), ); } #[test] fn test_positive() { - let x = PositiveNonzeroInteger::new(42); - assert!(x.is_ok()); - assert_eq!(parse_pos_nonzero("42"), Ok(x.unwrap())); + let x = PositiveNonzeroInteger::new(42).unwrap(); + assert_eq!(x.0, 42); + assert_eq!(PositiveNonzeroInteger::parse("42"), Ok(x)); } } diff --git a/exercises/14_generics/generics1.rs b/exercises/14_generics/generics1.rs index 35c1d2fe..87ed990b 100644 --- a/exercises/14_generics/generics1.rs +++ b/exercises/14_generics/generics1.rs @@ -1,14 +1,18 @@ -// generics1.rs -// -// This shopping list program isn't compiling! Use your knowledge of generics to -// fix it. -// -// Execute `rustlings hint generics1` or use the `hint` watch subcommand for a -// hint. - -// I AM NOT DONE +// `Vec` is generic over the type `T`. In most cases, the compiler is able to +// infer `T`, for example after pushing a value with a concrete type to the vector. +// But in this exercise, the compiler needs some help through a type annotation. fn main() { - let mut shopping_list: Vec = Vec::new(); - shopping_list.push("milk"); + // TODO: Fix the compiler error by annotating the type of the vector + // `Vec`. Choose `T` as some integer type that can be created from + // `u8` and `i8`. + let mut numbers = Vec::new(); + + // Don't change the lines below. + let n1: u8 = 42; + numbers.push(n1.into()); + let n2: i8 = -1; + numbers.push(n2.into()); + + println!("{numbers:?}"); } diff --git a/exercises/14_generics/generics2.rs b/exercises/14_generics/generics2.rs index 074cd938..8908725b 100644 --- a/exercises/14_generics/generics2.rs +++ b/exercises/14_generics/generics2.rs @@ -1,23 +1,20 @@ -// generics2.rs -// // This powerful wrapper provides the ability to store a positive integer value. -// Rewrite it using generics so that it supports wrapping ANY type. -// -// Execute `rustlings hint generics2` or use the `hint` watch subcommand for a -// hint. - -// I AM NOT DONE - +// TODO: Rewrite it using a generic so that it supports wrapping ANY type. struct Wrapper { value: u32, } +// TODO: Adapt the struct's implementation to be generic over the wrapped value. impl Wrapper { - pub fn new(value: u32) -> Self { + fn new(value: u32) -> Self { Wrapper { value } } } +fn main() { + // You can optionally experiment here. +} + #[cfg(test)] mod tests { use super::*; diff --git a/exercises/15_traits/traits1.rs b/exercises/15_traits/traits1.rs index 37dfcbfe..85be17ea 100644 --- a/exercises/15_traits/traits1.rs +++ b/exercises/15_traits/traits1.rs @@ -1,26 +1,17 @@ -// traits1.rs -// -// Time to implement some traits! Your task is to implement the trait -// `AppendBar` for the type `String`. The trait AppendBar has only one function, -// which appends "Bar" to any object implementing this trait. -// -// Execute `rustlings hint traits1` or use the `hint` watch subcommand for a -// hint. - -// I AM NOT DONE - +// The trait `AppendBar` has only one function which appends "Bar" to any object +// implementing this trait. trait AppendBar { fn append_bar(self) -> Self; } impl AppendBar for String { - // TODO: Implement `AppendBar` for type `String`. + // TODO: Implement `AppendBar` for the type `String`. } fn main() { let s = String::from("Foo"); let s = s.append_bar(); - println!("s: {}", s); + println!("s: {s}"); } #[cfg(test)] @@ -29,14 +20,11 @@ mod tests { #[test] fn is_foo_bar() { - assert_eq!(String::from("Foo").append_bar(), String::from("FooBar")); + assert_eq!(String::from("Foo").append_bar(), "FooBar"); } #[test] fn is_bar_bar() { - assert_eq!( - String::from("").append_bar().append_bar(), - String::from("BarBar") - ); + assert_eq!(String::from("").append_bar().append_bar(), "BarBar"); } } diff --git a/exercises/15_traits/traits2.rs b/exercises/15_traits/traits2.rs index 3e35f8e1..d724dc28 100644 --- a/exercises/15_traits/traits2.rs +++ b/exercises/15_traits/traits2.rs @@ -1,20 +1,13 @@ -// traits2.rs -// -// Your task is to implement the trait `AppendBar` for a vector of strings. To -// implement this trait, consider for a moment what it means to 'append "Bar"' -// to a vector of strings. -// -// No boiler plate code this time, you can do this! -// -// Execute `rustlings hint traits2` or use the `hint` watch subcommand for a hint. - -// I AM NOT DONE - trait AppendBar { fn append_bar(self) -> Self; } -// TODO: Implement trait `AppendBar` for a vector of strings. +// TODO: Implement the trait `AppendBar` for a vector of strings. +// `append_bar` should push the string "Bar" into the vector. + +fn main() { + // You can optionally experiment here. +} #[cfg(test)] mod tests { @@ -23,7 +16,7 @@ mod tests { #[test] fn is_vec_pop_eq_bar() { let mut foo = vec![String::from("Foo")].append_bar(); - assert_eq!(foo.pop().unwrap(), String::from("Bar")); - assert_eq!(foo.pop().unwrap(), String::from("Foo")); + assert_eq!(foo.pop().unwrap(), "Bar"); + assert_eq!(foo.pop().unwrap(), "Foo"); } } diff --git a/exercises/15_traits/traits3.rs b/exercises/15_traits/traits3.rs index 4e2b06b0..c244650d 100644 --- a/exercises/15_traits/traits3.rs +++ b/exercises/15_traits/traits3.rs @@ -1,16 +1,8 @@ -// traits3.rs -// -// Your task is to implement the Licensed trait for both structures and have -// them return the same information without writing the same function twice. -// -// Consider what you can add to the Licensed trait. -// -// Execute `rustlings hint traits3` or use the `hint` watch subcommand for a -// hint. - -// I AM NOT DONE - -pub trait Licensed { +trait Licensed { + // TODO: Add a default implementation for `licensing_info` so that + // implementors like the two structs below can share that default behavior + // without repeating the function. + // The default license information should be the string "Default license". fn licensing_info(&self) -> String; } @@ -22,8 +14,12 @@ struct OtherSoftware { version_number: String, } -impl Licensed for SomeSoftware {} // Don't edit this line -impl Licensed for OtherSoftware {} // Don't edit this line +impl Licensed for SomeSoftware {} // Don't edit this line. +impl Licensed for OtherSoftware {} // Don't edit this line. + +fn main() { + // You can optionally experiment here. +} #[cfg(test)] mod tests { @@ -31,7 +27,7 @@ mod tests { #[test] fn is_licensing_info_the_same() { - let licensing_info = String::from("Some information"); + let licensing_info = "Default license"; let some_software = SomeSoftware { version_number: 1 }; let other_software = OtherSoftware { version_number: "v2.0.0".to_string(), diff --git a/exercises/15_traits/traits4.rs b/exercises/15_traits/traits4.rs index 4bda3e57..80092a64 100644 --- a/exercises/15_traits/traits4.rs +++ b/exercises/15_traits/traits4.rs @@ -1,30 +1,22 @@ -// traits4.rs -// -// Your task is to replace the '??' sections so the code compiles. -// -// Don't change any line other than the marked one. -// -// Execute `rustlings hint traits4` or use the `hint` watch subcommand for a -// hint. - -// I AM NOT DONE - -pub trait Licensed { +trait Licensed { fn licensing_info(&self) -> String { - "some information".to_string() + "Default license".to_string() } } -struct SomeSoftware {} - -struct OtherSoftware {} +struct SomeSoftware; +struct OtherSoftware; impl Licensed for SomeSoftware {} impl Licensed for OtherSoftware {} -// YOU MAY ONLY CHANGE THE NEXT LINE -fn compare_license_types(software: ??, software_two: ??) -> bool { - software.licensing_info() == software_two.licensing_info() +// TODO: Fix the compiler error by only changing the signature of this function. +fn compare_license_types(software1: ???, software2: ???) -> bool { + software1.licensing_info() == software2.licensing_info() +} + +fn main() { + // You can optionally experiment here. } #[cfg(test)] @@ -33,17 +25,11 @@ mod tests { #[test] fn compare_license_information() { - let some_software = SomeSoftware {}; - let other_software = OtherSoftware {}; - - assert!(compare_license_types(some_software, other_software)); + assert!(compare_license_types(SomeSoftware, OtherSoftware)); } #[test] fn compare_license_information_backwards() { - let some_software = SomeSoftware {}; - let other_software = OtherSoftware {}; - - assert!(compare_license_types(other_software, some_software)); + assert!(compare_license_types(OtherSoftware, SomeSoftware)); } } diff --git a/exercises/15_traits/traits5.rs b/exercises/15_traits/traits5.rs index df183805..5b356ac7 100644 --- a/exercises/15_traits/traits5.rs +++ b/exercises/15_traits/traits5.rs @@ -1,40 +1,39 @@ -// traits5.rs -// -// Your task is to replace the '??' sections so the code compiles. -// -// Don't change any line other than the marked one. -// -// Execute `rustlings hint traits5` or use the `hint` watch subcommand for a -// hint. - -// I AM NOT DONE - -pub trait SomeTrait { +trait SomeTrait { fn some_function(&self) -> bool { true } } -pub trait OtherTrait { +trait OtherTrait { fn other_function(&self) -> bool { true } } -struct SomeStruct {} -struct OtherStruct {} - +struct SomeStruct; impl SomeTrait for SomeStruct {} impl OtherTrait for SomeStruct {} + +struct OtherStruct; impl SomeTrait for OtherStruct {} impl OtherTrait for OtherStruct {} -// YOU MAY ONLY CHANGE THE NEXT LINE -fn some_func(item: ??) -> bool { +// TODO: Fix the compiler error by only changing the signature of this function. +fn some_func(item: ???) -> bool { item.some_function() && item.other_function() } fn main() { - some_func(SomeStruct {}); - some_func(OtherStruct {}); + // You can optionally experiment here. +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_some_func() { + assert!(some_func(SomeStruct)); + assert!(some_func(OtherStruct)); + } } diff --git a/exercises/16_lifetimes/lifetimes1.rs b/exercises/16_lifetimes/lifetimes1.rs index 87bde490..19e2d398 100644 --- a/exercises/16_lifetimes/lifetimes1.rs +++ b/exercises/16_lifetimes/lifetimes1.rs @@ -1,15 +1,9 @@ -// lifetimes1.rs -// // The Rust compiler needs to know how to check whether supplied references are // valid, so that it can let the programmer know if a reference is at risk of // going out of scope before it is used. Remember, references are borrows and do // not own their own data. What if their owner goes out of scope? -// -// Execute `rustlings hint lifetimes1` or use the `hint` watch subcommand for a -// hint. - -// I AM NOT DONE +// TODO: Fix the compiler error by updating the function signature. fn longest(x: &str, y: &str) -> &str { if x.len() > y.len() { x @@ -19,9 +13,16 @@ fn longest(x: &str, y: &str) -> &str { } fn main() { - let string1 = String::from("abcd"); - let string2 = "xyz"; - - let result = longest(string1.as_str(), string2); - println!("The longest string is '{}'", result); + // You can optionally experiment here. +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_longest() { + assert_eq!(longest("abcd", "123"), "abcd"); + assert_eq!(longest("abc", "1234"), "1234"); + } } diff --git a/exercises/16_lifetimes/lifetimes2.rs b/exercises/16_lifetimes/lifetimes2.rs index 4f3d8c18..de5a5dfc 100644 --- a/exercises/16_lifetimes/lifetimes2.rs +++ b/exercises/16_lifetimes/lifetimes2.rs @@ -1,13 +1,4 @@ -// lifetimes2.rs -// -// So if the compiler is just validating the references passed to the annotated -// parameters and the return type, what do we need to change? -// -// Execute `rustlings hint lifetimes2` or use the `hint` watch subcommand for a -// hint. - -// I AM NOT DONE - +// Don't change this function. fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { if x.len() > y.len() { x @@ -17,11 +8,13 @@ fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { } fn main() { + // TODO: Fix the compiler error by moving one line. + let string1 = String::from("long string is long"); let result; { let string2 = String::from("xyz"); - result = longest(string1.as_str(), string2.as_str()); + result = longest(&string1, &string2); } - println!("The longest string is '{}'", result); + println!("The longest string is '{result}'"); } diff --git a/exercises/16_lifetimes/lifetimes3.rs b/exercises/16_lifetimes/lifetimes3.rs index 9c59f9c0..1cc27592 100644 --- a/exercises/16_lifetimes/lifetimes3.rs +++ b/exercises/16_lifetimes/lifetimes3.rs @@ -1,21 +1,16 @@ -// lifetimes3.rs -// // Lifetimes are also needed when structs hold references. -// -// Execute `rustlings hint lifetimes3` or use the `hint` watch subcommand for a -// hint. - -// I AM NOT DONE +// TODO: Fix the compiler errors about the struct. struct Book { author: &str, title: &str, } fn main() { - let name = String::from("Jill Smith"); - let title = String::from("Fish Flying"); - let book = Book { author: &name, title: &title }; + let book = Book { + author: "George Orwell", + title: "1984", + }; println!("{} by {}", book.title, book.author); } diff --git a/exercises/17_tests/tests1.rs b/exercises/17_tests/tests1.rs index 810277ac..7529f9f0 100644 --- a/exercises/17_tests/tests1.rs +++ b/exercises/17_tests/tests1.rs @@ -1,21 +1,23 @@ -// tests1.rs -// // Tests are important to ensure that your code does what you think it should -// do. Tests can be run on this file with the following command: rustlings run -// tests1 -// -// This test has a problem with it -- make the test compile! Make the test pass! -// Make the test fail! -// -// Execute `rustlings hint tests1` or use the `hint` watch subcommand for a -// hint. +// do. -// I AM NOT DONE +fn is_even(n: i64) -> bool { + n % 2 == 0 +} + +fn main() { + // You can optionally experiment here. +} #[cfg(test)] mod tests { + // TODO: Import `is_even`. You can use a wildcard to import everything in + // the outer module. + #[test] fn you_can_assert() { + // TODO: Test the function `is_even` with some values. + assert!(); assert!(); } } diff --git a/exercises/17_tests/tests2.rs b/exercises/17_tests/tests2.rs index f8024e9f..0c6573e8 100644 --- a/exercises/17_tests/tests2.rs +++ b/exercises/17_tests/tests2.rs @@ -1,17 +1,23 @@ -// tests2.rs -// -// This test has a problem with it -- make the test compile! Make the test pass! -// Make the test fail! -// -// Execute `rustlings hint tests2` or use the `hint` watch subcommand for a -// hint. +// Calculates the power of 2 using a bit shift. +// `1 << n` is equivalent to "2 to the power of n". +fn power_of_2(n: u8) -> u64 { + 1 << n +} -// I AM NOT DONE +fn main() { + // You can optionally experiment here. +} #[cfg(test)] mod tests { + use super::*; + #[test] fn you_can_assert_eq() { + // TODO: Test the function `power_of_2` with some values. + assert_eq!(); + assert_eq!(); + assert_eq!(); assert_eq!(); } } diff --git a/exercises/17_tests/tests3.rs b/exercises/17_tests/tests3.rs index 4013e384..ec994792 100644 --- a/exercises/17_tests/tests3.rs +++ b/exercises/17_tests/tests3.rs @@ -1,16 +1,23 @@ -// tests3.rs -// -// This test isn't testing our function -- make it do that in such a way that -// the test passes. Then write a second test that tests whether we get the -// result we expect to get when we call `is_even(5)`. -// -// Execute `rustlings hint tests3` or use the `hint` watch subcommand for a -// hint. +struct Rectangle { + width: i32, + height: i32, +} -// I AM NOT DONE +impl Rectangle { + // Don't change this function. + fn new(width: i32, height: i32) -> Self { + if width <= 0 || height <= 0 { + // Returning a `Result` would be better here. But we want to learn + // how to test functions that can panic. + panic!("Rectangle width and height can't be negative"); + } -pub fn is_even(num: i32) -> bool { - num % 2 == 0 + Rectangle { width, height } + } +} + +fn main() { + // You can optionally experiment here. } #[cfg(test)] @@ -18,12 +25,25 @@ mod tests { use super::*; #[test] - fn is_true_when_even() { - assert!(); + fn correct_width_and_height() { + // TODO: This test should check if the rectangle has the size that we + // pass to its constructor. + let rect = Rectangle::new(10, 20); + assert_eq!(todo!(), 10); // Check width + assert_eq!(todo!(), 20); // Check height } + // TODO: This test should check if the program panics when we try to create + // a rectangle with negative width. #[test] - fn is_false_when_odd() { - assert!(); + fn negative_width() { + let _rect = Rectangle::new(-10, 10); + } + + // TODO: This test should check if the program panics when we try to create + // a rectangle with negative height. + #[test] + fn negative_height() { + let _rect = Rectangle::new(10, -10); } } diff --git a/exercises/17_tests/tests4.rs b/exercises/17_tests/tests4.rs deleted file mode 100644 index 935d0db1..00000000 --- a/exercises/17_tests/tests4.rs +++ /dev/null @@ -1,48 +0,0 @@ -// tests4.rs -// -// Make sure that we're testing for the correct conditions! -// -// Execute `rustlings hint tests4` or use the `hint` watch subcommand for a -// hint. - -// I AM NOT DONE - -struct Rectangle { - width: i32, - height: i32 -} - -impl Rectangle { - // Only change the test functions themselves - pub fn new(width: i32, height: i32) -> Self { - if width <= 0 || height <= 0 { - panic!("Rectangle width and height cannot be negative!") - } - Rectangle {width, height} - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn correct_width_and_height() { - // This test should check if the rectangle is the size that we pass into its constructor - let rect = Rectangle::new(10, 20); - assert_eq!(???, 10); // check width - assert_eq!(???, 20); // check height - } - - #[test] - fn negative_width() { - // This test should check if program panics when we try to create rectangle with negative width - let _rect = Rectangle::new(-10, 10); - } - - #[test] - fn negative_height() { - // This test should check if program panics when we try to create rectangle with negative height - let _rect = Rectangle::new(10, -10); - } -} diff --git a/exercises/18_iterators/iterators1.rs b/exercises/18_iterators/iterators1.rs index 31076bb9..ca937ed0 100644 --- a/exercises/18_iterators/iterators1.rs +++ b/exercises/18_iterators/iterators1.rs @@ -1,26 +1,25 @@ -// iterators1.rs -// // When performing operations on elements within a collection, iterators are // essential. This module helps you get familiar with the structure of using an // iterator and how to go through elements within an iterable collection. -// -// Make me compile by filling in the `???`s -// -// Execute `rustlings hint iterators1` or use the `hint` watch subcommand for a -// hint. -// I AM NOT DONE - -#[test] fn main() { - let my_fav_fruits = vec!["banana", "custard apple", "avocado", "peach", "raspberry"]; - - let mut my_iterable_fav_fruits = ???; // TODO: Step 1 - - assert_eq!(my_iterable_fav_fruits.next(), Some(&"banana")); - assert_eq!(my_iterable_fav_fruits.next(), ???); // TODO: Step 2 - assert_eq!(my_iterable_fav_fruits.next(), Some(&"avocado")); - assert_eq!(my_iterable_fav_fruits.next(), ???); // TODO: Step 3 - assert_eq!(my_iterable_fav_fruits.next(), Some(&"raspberry")); - assert_eq!(my_iterable_fav_fruits.next(), ???); // TODO: Step 4 + // You can optionally experiment here. +} + +#[cfg(test)] +mod tests { + #[test] + fn iterators() { + let my_fav_fruits = ["banana", "custard apple", "avocado", "peach", "raspberry"]; + + // TODO: Create an iterator over the array. + let mut fav_fruits_iterator = todo!(); + + assert_eq!(fav_fruits_iterator.next(), Some(&"banana")); + assert_eq!(fav_fruits_iterator.next(), todo!()); // TODO: Replace `todo!()` + assert_eq!(fav_fruits_iterator.next(), Some(&"avocado")); + assert_eq!(fav_fruits_iterator.next(), todo!()); // TODO: Replace `todo!()` + assert_eq!(fav_fruits_iterator.next(), Some(&"raspberry")); + assert_eq!(fav_fruits_iterator.next(), todo!()); // TODO: Replace `todo!()` + } } diff --git a/exercises/18_iterators/iterators2.rs b/exercises/18_iterators/iterators2.rs index dda82a08..5903e657 100644 --- a/exercises/18_iterators/iterators2.rs +++ b/exercises/18_iterators/iterators2.rs @@ -1,38 +1,32 @@ -// iterators2.rs -// // In this exercise, you'll learn some of the unique advantages that iterators -// can offer. Follow the steps to complete the exercise. -// -// Execute `rustlings hint iterators2` or use the `hint` watch subcommand for a -// hint. +// can offer. -// I AM NOT DONE - -// Step 1. -// Complete the `capitalize_first` function. +// TODO: Complete the `capitalize_first` function. // "hello" -> "Hello" -pub fn capitalize_first(input: &str) -> String { - let mut c = input.chars(); - match c.next() { +fn capitalize_first(input: &str) -> String { + let mut chars = input.chars(); + match chars.next() { None => String::new(), - Some(first) => ???, + Some(first) => todo!(), } } -// Step 2. -// Apply the `capitalize_first` function to a slice of string slices. +// TODO: Apply the `capitalize_first` function to a slice of string slices. // Return a vector of strings. // ["hello", "world"] -> ["Hello", "World"] -pub fn capitalize_words_vector(words: &[&str]) -> Vec { - vec![] +fn capitalize_words_vector(words: &[&str]) -> Vec { + // ??? } -// Step 3. -// Apply the `capitalize_first` function again to a slice of string slices. -// Return a single string. +// TODO: Apply the `capitalize_first` function again to a slice of string +// slices. Return a single string. // ["hello", " ", "world"] -> "Hello World" -pub fn capitalize_words_string(words: &[&str]) -> String { - String::new() +fn capitalize_words_string(words: &[&str]) -> String { + // ??? +} + +fn main() { + // You can optionally experiment here. } #[cfg(test)] diff --git a/exercises/18_iterators/iterators3.rs b/exercises/18_iterators/iterators3.rs index 29fa23a3..b5d05f6e 100644 --- a/exercises/18_iterators/iterators3.rs +++ b/exercises/18_iterators/iterators3.rs @@ -1,50 +1,33 @@ -// iterators3.rs -// -// This is a bigger exercise than most of the others! You can do it! Here is -// your mission, should you choose to accept it: -// 1. Complete the divide function to get the first four tests to pass. -// 2. Get the remaining tests to pass by completing the result_with_list and -// list_of_results functions. -// -// Execute `rustlings hint iterators3` or use the `hint` watch subcommand for a -// hint. - -// I AM NOT DONE - #[derive(Debug, PartialEq, Eq)] -pub enum DivisionError { - NotDivisible(NotDivisibleError), +enum DivisionError { DivideByZero, + NotDivisible, } -#[derive(Debug, PartialEq, Eq)] -pub struct NotDivisibleError { - dividend: i32, - divisor: i32, -} - -// Calculate `a` divided by `b` if `a` is evenly divisible by `b`. +// TODO: Calculate `a` divided by `b` if `a` is evenly divisible by `b`. // Otherwise, return a suitable error. -pub fn divide(a: i32, b: i32) -> Result { +fn divide(a: i32, b: i32) -> Result { todo!(); } -// Complete the function and return a value of the correct type so the test -// passes. -// Desired output: Ok([1, 11, 1426, 3]) -fn result_with_list() -> () { - let numbers = vec![27, 297, 38502, 81]; +// TODO: Add the correct return type and complete the function body. +// Desired output: `Ok([1, 11, 1426, 3])` +fn result_with_list() { + let numbers = [27, 297, 38502, 81]; let division_results = numbers.into_iter().map(|n| divide(n, 27)); } -// Complete the function and return a value of the correct type so the test -// passes. -// Desired output: [Ok(1), Ok(11), Ok(1426), Ok(3)] -fn list_of_results() -> () { - let numbers = vec![27, 297, 38502, 81]; +// TODO: Add the correct return type and complete the function body. +// Desired output: `[Ok(1), Ok(11), Ok(1426), Ok(3)]` +fn list_of_results() { + let numbers = [27, 297, 38502, 81]; let division_results = numbers.into_iter().map(|n| divide(n, 27)); } +fn main() { + // You can optionally experiment here. +} + #[cfg(test)] mod tests { use super::*; @@ -55,19 +38,13 @@ mod tests { } #[test] - fn test_not_divisible() { - assert_eq!( - divide(81, 6), - Err(DivisionError::NotDivisible(NotDivisibleError { - dividend: 81, - divisor: 6 - })) - ); + fn test_divide_by_0() { + assert_eq!(divide(81, 0), Err(DivisionError::DivideByZero)); } #[test] - fn test_divide_by_0() { - assert_eq!(divide(81, 0), Err(DivisionError::DivideByZero)); + fn test_not_divisible() { + assert_eq!(divide(81, 6), Err(DivisionError::NotDivisible)); } #[test] @@ -77,14 +54,11 @@ mod tests { #[test] fn test_result_with_list() { - assert_eq!(format!("{:?}", result_with_list()), "Ok([1, 11, 1426, 3])"); + assert_eq!(result_with_list().unwarp(), [1, 11, 1426, 3]); } #[test] fn test_list_of_results() { - assert_eq!( - format!("{:?}", list_of_results()), - "[Ok(1), Ok(11), Ok(1426), Ok(3)]" - ); + assert_eq!(list_of_results(), [Ok(1), Ok(11), Ok(1426), Ok(3)]); } } diff --git a/exercises/18_iterators/iterators4.rs b/exercises/18_iterators/iterators4.rs index 3c0724e9..08ba3650 100644 --- a/exercises/18_iterators/iterators4.rs +++ b/exercises/18_iterators/iterators4.rs @@ -1,20 +1,16 @@ -// iterators4.rs -// -// Execute `rustlings hint iterators4` or use the `hint` watch subcommand for a -// hint. - -// I AM NOT DONE - -pub fn factorial(num: u64) -> u64 { - // Complete this function to return the factorial of num +fn factorial(num: u8) -> u64 { + // TODO: Complete this function to return the factorial of `num`. // Do not use: // - early returns (using the `return` keyword explicitly) // Try not to use: - // - imperative style loops (for, while) + // - imperative style loops (for/while) // - additional variables // For an extra challenge, don't use: // - recursion - // Execute `rustlings hint iterators4` for hints. +} + +fn main() { + // You can optionally experiment here. } #[cfg(test)] @@ -23,20 +19,20 @@ mod tests { #[test] fn factorial_of_0() { - assert_eq!(1, factorial(0)); + assert_eq!(factorial(0), 1); } #[test] fn factorial_of_1() { - assert_eq!(1, factorial(1)); + assert_eq!(factorial(1), 1); } #[test] fn factorial_of_2() { - assert_eq!(2, factorial(2)); + assert_eq!(factorial(2), 2); } #[test] fn factorial_of_4() { - assert_eq!(24, factorial(4)); + assert_eq!(factorial(4), 24); } } diff --git a/exercises/18_iterators/iterators5.rs b/exercises/18_iterators/iterators5.rs index a062ee4c..7e434cc5 100644 --- a/exercises/18_iterators/iterators5.rs +++ b/exercises/18_iterators/iterators5.rs @@ -1,17 +1,8 @@ -// iterators5.rs -// -// Let's define a simple model to track Rustlings exercise progress. Progress +// Let's define a simple model to track Rustlings' exercise progress. Progress // will be modelled using a hash map. The name of the exercise is the key and // the progress is the value. Two counting functions were created to count the // number of exercises with a given progress. Recreate this counting -// functionality using iterators. Try not to use imperative loops (for, while). -// Only the two iterator methods (count_iterator and count_collection_iterator) -// need to be modified. -// -// Execute `rustlings hint iterators5` or use the `hint` watch subcommand for a -// hint. - -// I AM NOT DONE +// functionality using iterators. Try to not use imperative loops (for/while). use std::collections::HashMap; @@ -25,24 +16,25 @@ enum Progress { fn count_for(map: &HashMap, value: Progress) -> usize { let mut count = 0; for val in map.values() { - if val == &value { + if *val == value { count += 1; } } count } +// TODO: Implement the functionality of `count_for` but with an iterator instead +// of a `for` loop. fn count_iterator(map: &HashMap, value: Progress) -> usize { - // map is a hashmap with String keys and Progress values. - // map = { "variables1": Complete, "from_str": None, ... } - todo!(); + // `map` is a hash map with `String` keys and `Progress` values. + // map = { "variables1": Complete, "from_str": None, … } } fn count_collection_for(collection: &[HashMap], value: Progress) -> usize { let mut count = 0; for map in collection { for val in map.values() { - if val == &value { + if *val == value { count += 1; } } @@ -50,81 +42,22 @@ fn count_collection_for(collection: &[HashMap], value: Progres count } +// TODO: Implement the functionality of `count_collection_for` but with an +// iterator instead of a `for` loop. fn count_collection_iterator(collection: &[HashMap], value: Progress) -> usize { - // collection is a slice of hashmaps. - // collection = [{ "variables1": Complete, "from_str": None, ... }, - // { "variables2": Complete, ... }, ... ] - todo!(); + // `collection` is a slice of hash maps. + // collection = [{ "variables1": Complete, "from_str": None, … }, + // { "variables2": Complete, … }, … ] +} + +fn main() { + // You can optionally experiment here. } #[cfg(test)] mod tests { use super::*; - #[test] - fn count_complete() { - let map = get_map(); - assert_eq!(3, count_iterator(&map, Progress::Complete)); - } - - #[test] - fn count_some() { - let map = get_map(); - assert_eq!(1, count_iterator(&map, Progress::Some)); - } - - #[test] - fn count_none() { - let map = get_map(); - assert_eq!(2, count_iterator(&map, Progress::None)); - } - - #[test] - fn count_complete_equals_for() { - let map = get_map(); - let progress_states = vec![Progress::Complete, Progress::Some, Progress::None]; - for progress_state in progress_states { - assert_eq!( - count_for(&map, progress_state), - count_iterator(&map, progress_state) - ); - } - } - - #[test] - fn count_collection_complete() { - let collection = get_vec_map(); - assert_eq!( - 6, - count_collection_iterator(&collection, Progress::Complete) - ); - } - - #[test] - fn count_collection_some() { - let collection = get_vec_map(); - assert_eq!(1, count_collection_iterator(&collection, Progress::Some)); - } - - #[test] - fn count_collection_none() { - let collection = get_vec_map(); - assert_eq!(4, count_collection_iterator(&collection, Progress::None)); - } - - #[test] - fn count_collection_equals_for() { - let progress_states = vec![Progress::Complete, Progress::Some, Progress::None]; - let collection = get_vec_map(); - - for progress_state in progress_states { - assert_eq!( - count_collection_for(&collection, progress_state), - count_collection_iterator(&collection, progress_state) - ); - } - } - fn get_map() -> HashMap { use Progress::*; @@ -153,4 +86,68 @@ mod tests { vec![map, other] } + + #[test] + fn count_complete() { + let map = get_map(); + assert_eq!(count_iterator(&map, Progress::Complete), 3); + } + + #[test] + fn count_some() { + let map = get_map(); + assert_eq!(count_iterator(&map, Progress::Some), 1); + } + + #[test] + fn count_none() { + let map = get_map(); + assert_eq!(count_iterator(&map, Progress::None), 2); + } + + #[test] + fn count_complete_equals_for() { + let map = get_map(); + let progress_states = [Progress::Complete, Progress::Some, Progress::None]; + for progress_state in progress_states { + assert_eq!( + count_for(&map, progress_state), + count_iterator(&map, progress_state), + ); + } + } + + #[test] + fn count_collection_complete() { + let collection = get_vec_map(); + assert_eq!( + count_collection_iterator(&collection, Progress::Complete), + 6, + ); + } + + #[test] + fn count_collection_some() { + let collection = get_vec_map(); + assert_eq!(count_collection_iterator(&collection, Progress::Some), 1); + } + + #[test] + fn count_collection_none() { + let collection = get_vec_map(); + assert_eq!(count_collection_iterator(&collection, Progress::None), 4); + } + + #[test] + fn count_collection_equals_for() { + let collection = get_vec_map(); + let progress_states = [Progress::Complete, Progress::Some, Progress::None]; + + for progress_state in progress_states { + assert_eq!( + count_collection_for(&collection, progress_state), + count_collection_iterator(&collection, progress_state), + ); + } + } } diff --git a/exercises/19_smart_pointers/arc1.rs b/exercises/19_smart_pointers/arc1.rs index 3526ddcb..c3d714dc 100644 --- a/exercises/19_smart_pointers/arc1.rs +++ b/exercises/19_smart_pointers/arc1.rs @@ -1,45 +1,42 @@ -// arc1.rs +// In this exercise, we are given a `Vec` of u32 called `numbers` with values +// ranging from 0 to 99. We would like to use this set of numbers within 8 +// different threads simultaneously. Each thread is going to get the sum of +// every eighth value with an offset. // -// In this exercise, we are given a Vec of u32 called "numbers" with values -// ranging from 0 to 99 -- [ 0, 1, 2, ..., 98, 99 ] We would like to use this -// set of numbers within 8 different threads simultaneously. Each thread is -// going to get the sum of every eighth value, with an offset. +// The first thread (offset 0), will sum 0, 8, 16, … +// The second thread (offset 1), will sum 1, 9, 17, … +// The third thread (offset 2), will sum 2, 10, 18, … +// … +// The eighth thread (offset 7), will sum 7, 15, 23, … // -// The first thread (offset 0), will sum 0, 8, 16, ... -// The second thread (offset 1), will sum 1, 9, 17, ... -// The third thread (offset 2), will sum 2, 10, 18, ... -// ... -// The eighth thread (offset 7), will sum 7, 15, 23, ... -// -// Because we are using threads, our values need to be thread-safe. Therefore, -// we are using Arc. We need to make a change in each of the two TODOs. -// -// Make this code compile by filling in a value for `shared_numbers` where the -// first TODO comment is, and create an initial binding for `child_numbers` -// where the second TODO comment is. Try not to create any copies of the -// `numbers` Vec! -// -// Execute `rustlings hint arc1` or use the `hint` watch subcommand for a hint. +// Because we are using threads, our values need to be thread-safe. Therefore, +// we are using `Arc`. -// I AM NOT DONE - -#![forbid(unused_imports)] // Do not change this, (or the next) line. -use std::sync::Arc; -use std::thread; +// Don't change the lines below. +#![forbid(unused_imports)] +use std::{sync::Arc, thread}; fn main() { let numbers: Vec<_> = (0..100u32).collect(); - let shared_numbers = // TODO - let mut joinhandles = Vec::new(); + + // TODO: Define `shared_numbers` by using `Arc`. + // let shared_numbers = ???; + + let mut join_handles = Vec::new(); for offset in 0..8 { - let child_numbers = // TODO - joinhandles.push(thread::spawn(move || { + // TODO: Define `child_numbers` using `shared_numbers`. + // let child_numbers = ???; + + let handle = thread::spawn(move || { let sum: u32 = child_numbers.iter().filter(|&&n| n % 8 == offset).sum(); - println!("Sum of offset {} is {}", offset, sum); - })); + println!("Sum of offset {offset} is {sum}"); + }); + + join_handles.push(handle); } - for handle in joinhandles.into_iter() { + + for handle in join_handles.into_iter() { handle.join().unwrap(); } } diff --git a/exercises/19_smart_pointers/box1.rs b/exercises/19_smart_pointers/box1.rs index 513e7daa..d70e1c3d 100644 --- a/exercises/19_smart_pointers/box1.rs +++ b/exercises/19_smart_pointers/box1.rs @@ -1,58 +1,50 @@ -// box1.rs -// // At compile time, Rust needs to know how much space a type takes up. This // becomes problematic for recursive types, where a value can have as part of // itself another value of the same type. To get around the issue, we can use a // `Box` - a smart pointer used to store data on the heap, which also allows us // to wrap a recursive type. // -// The recursive type we're implementing in this exercise is the `cons list` - a +// The recursive type we're implementing in this exercise is the "cons list", a // data structure frequently found in functional programming languages. Each -// item in a cons list contains two elements: the value of the current item and +// item in a cons list contains two elements: The value of the current item and // the next item. The last item is a value called `Nil`. -// -// Step 1: use a `Box` in the enum definition to make the code compile -// Step 2: create both empty and non-empty cons lists by replacing `todo!()` -// -// Note: the tests should not be changed -// -// Execute `rustlings hint box1` or use the `hint` watch subcommand for a hint. - -// I AM NOT DONE +// TODO: Use a `Box` in the enum definition to make the code compile. #[derive(PartialEq, Debug)] -pub enum List { +enum List { Cons(i32, List), Nil, } +// TODO: Create an empty cons list. +fn create_empty_list() -> List { + todo!() +} + +// TODO: Create a non-empty cons list. +fn create_non_empty_list() -> List { + todo!() +} + fn main() { println!("This is an empty cons list: {:?}", create_empty_list()); println!( "This is a non-empty cons list: {:?}", - create_non_empty_list() + create_non_empty_list(), ); } -pub fn create_empty_list() -> List { - todo!() -} - -pub fn create_non_empty_list() -> List { - todo!() -} - #[cfg(test)] mod tests { use super::*; #[test] fn test_create_empty_list() { - assert_eq!(List::Nil, create_empty_list()) + assert_eq!(create_empty_list(), List::Nil); } #[test] fn test_create_non_empty_list() { - assert_ne!(create_empty_list(), create_non_empty_list()) + assert_ne!(create_empty_list(), create_non_empty_list()); } } diff --git a/exercises/19_smart_pointers/cow1.rs b/exercises/19_smart_pointers/cow1.rs index fcd3e0bb..5ecf8482 100644 --- a/exercises/19_smart_pointers/cow1.rs +++ b/exercises/19_smart_pointers/cow1.rs @@ -1,30 +1,22 @@ -// cow1.rs -// -// This exercise explores the Cow, or Clone-On-Write type. Cow is a -// clone-on-write smart pointer. It can enclose and provide immutable access to -// borrowed data, and clone the data lazily when mutation or ownership is -// required. The type is designed to work with general borrowed data via the -// Borrow trait. -// -// This exercise is meant to show you what to expect when passing data to Cow. -// Fix the unit tests by checking for Cow::Owned(_) and Cow::Borrowed(_) at the -// TODO markers. -// -// Execute `rustlings hint cow1` or use the `hint` watch subcommand for a hint. - -// I AM NOT DONE +// This exercise explores the `Cow` (Clone-On-Write) smart pointer. It can +// enclose and provide immutable access to borrowed data and clone the data +// lazily when mutation or ownership is required. The type is designed to work +// with general borrowed data via the `Borrow` trait. use std::borrow::Cow; -fn abs_all<'a, 'b>(input: &'a mut Cow<'b, [i32]>) -> &'a mut Cow<'b, [i32]> { - for i in 0..input.len() { - let v = input[i]; - if v < 0 { +fn abs_all(input: &mut Cow<[i32]>) { + for ind in 0..input.len() { + let value = input[ind]; + if value < 0 { // Clones into a vector if not already owned. - input.to_mut()[i] = -v; + input.to_mut()[ind] = -value; } } - input +} + +fn main() { + // You can optionally experiment here. } #[cfg(test)] @@ -32,47 +24,45 @@ mod tests { use super::*; #[test] - fn reference_mutation() -> Result<(), &'static str> { + fn reference_mutation() { // Clone occurs because `input` needs to be mutated. - let slice = [-1, 0, 1]; - let mut input = Cow::from(&slice[..]); - match abs_all(&mut input) { - Cow::Owned(_) => Ok(()), - _ => Err("Expected owned value"), - } + let vec = vec![-1, 0, 1]; + let mut input = Cow::from(&vec); + abs_all(&mut input); + assert!(matches!(input, Cow::Owned(_))); } #[test] - fn reference_no_mutation() -> Result<(), &'static str> { + fn reference_no_mutation() { // No clone occurs because `input` doesn't need to be mutated. - let slice = [0, 1, 2]; - let mut input = Cow::from(&slice[..]); - match abs_all(&mut input) { - // TODO - } + let vec = vec![0, 1, 2]; + let mut input = Cow::from(&vec); + abs_all(&mut input); + // TODO: Replace `todo!()` with `Cow::Owned(_)` or `Cow::Borrowed(_)`. + assert!(matches!(input, todo!())); } #[test] - fn owned_no_mutation() -> Result<(), &'static str> { - // We can also pass `slice` without `&` so Cow owns it directly. In this - // case no mutation occurs and thus also no clone, but the result is + fn owned_no_mutation() { + // We can also pass `vec` without `&` so `Cow` owns it directly. In this + // case, no mutation occurs and thus also no clone. But the result is // still owned because it was never borrowed or mutated. - let slice = vec![0, 1, 2]; - let mut input = Cow::from(slice); - match abs_all(&mut input) { - // TODO - } + let vec = vec![0, 1, 2]; + let mut input = Cow::from(vec); + abs_all(&mut input); + // TODO: Replace `todo!()` with `Cow::Owned(_)` or `Cow::Borrowed(_)`. + assert!(matches!(input, todo!())); } #[test] - fn owned_mutation() -> Result<(), &'static str> { + fn owned_mutation() { // Of course this is also the case if a mutation does occur. In this - // case the call to `to_mut()` in the abs_all() function returns a + // case, the call to `to_mut()` in the `abs_all` function returns a // reference to the same data as before. - let slice = vec![-1, 0, 1]; - let mut input = Cow::from(slice); - match abs_all(&mut input) { - // TODO - } + let vec = vec![-1, 0, 1]; + let mut input = Cow::from(vec); + abs_all(&mut input); + // TODO: Replace `todo!()` with `Cow::Owned(_)` or `Cow::Borrowed(_)`. + assert!(matches!(input, todo!())); } } diff --git a/exercises/19_smart_pointers/rc1.rs b/exercises/19_smart_pointers/rc1.rs index 1b903469..ecd34387 100644 --- a/exercises/19_smart_pointers/rc1.rs +++ b/exercises/19_smart_pointers/rc1.rs @@ -1,21 +1,12 @@ -// rc1.rs -// // In this exercise, we want to express the concept of multiple owners via the -// Rc type. This is a model of our solar system - there is a Sun type and -// multiple Planets. The Planets take ownership of the sun, indicating that they -// revolve around the sun. -// -// Make this code compile by using the proper Rc primitives to express that the -// sun has multiple owners. -// -// Execute `rustlings hint rc1` or use the `hint` watch subcommand for a hint. - -// I AM NOT DONE +// `Rc` type. This is a model of our solar system - there is a `Sun` type and +// multiple `Planet`s. The planets take ownership of the sun, indicating that +// they revolve around the sun. use std::rc::Rc; #[derive(Debug)] -struct Sun {} +struct Sun; #[derive(Debug)] enum Planet { @@ -31,75 +22,84 @@ enum Planet { impl Planet { fn details(&self) { - println!("Hi from {:?}!", self) + println!("Hi from {self:?}!"); } } -#[test] fn main() { - let sun = Rc::new(Sun {}); - println!("reference count = {}", Rc::strong_count(&sun)); // 1 reference - - let mercury = Planet::Mercury(Rc::clone(&sun)); - println!("reference count = {}", Rc::strong_count(&sun)); // 2 references - mercury.details(); - - let venus = Planet::Venus(Rc::clone(&sun)); - println!("reference count = {}", Rc::strong_count(&sun)); // 3 references - venus.details(); - - let earth = Planet::Earth(Rc::clone(&sun)); - println!("reference count = {}", Rc::strong_count(&sun)); // 4 references - earth.details(); - - let mars = Planet::Mars(Rc::clone(&sun)); - println!("reference count = {}", Rc::strong_count(&sun)); // 5 references - mars.details(); - - let jupiter = Planet::Jupiter(Rc::clone(&sun)); - println!("reference count = {}", Rc::strong_count(&sun)); // 6 references - jupiter.details(); - - // TODO - let saturn = Planet::Saturn(Rc::new(Sun {})); - println!("reference count = {}", Rc::strong_count(&sun)); // 7 references - saturn.details(); - - // TODO - let uranus = Planet::Uranus(Rc::new(Sun {})); - println!("reference count = {}", Rc::strong_count(&sun)); // 8 references - uranus.details(); - - // TODO - let neptune = Planet::Neptune(Rc::new(Sun {})); - println!("reference count = {}", Rc::strong_count(&sun)); // 9 references - neptune.details(); - - assert_eq!(Rc::strong_count(&sun), 9); - - drop(neptune); - println!("reference count = {}", Rc::strong_count(&sun)); // 8 references - - drop(uranus); - println!("reference count = {}", Rc::strong_count(&sun)); // 7 references - - drop(saturn); - println!("reference count = {}", Rc::strong_count(&sun)); // 6 references - - drop(jupiter); - println!("reference count = {}", Rc::strong_count(&sun)); // 5 references - - drop(mars); - println!("reference count = {}", Rc::strong_count(&sun)); // 4 references - - // TODO - println!("reference count = {}", Rc::strong_count(&sun)); // 3 references - - // TODO - println!("reference count = {}", Rc::strong_count(&sun)); // 2 references - - // TODO - println!("reference count = {}", Rc::strong_count(&sun)); // 1 reference - - assert_eq!(Rc::strong_count(&sun), 1); + // You can optionally experiment here. +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn rc1() { + let sun = Rc::new(Sun); + println!("reference count = {}", Rc::strong_count(&sun)); // 1 reference + + let mercury = Planet::Mercury(Rc::clone(&sun)); + println!("reference count = {}", Rc::strong_count(&sun)); // 2 references + mercury.details(); + + let venus = Planet::Venus(Rc::clone(&sun)); + println!("reference count = {}", Rc::strong_count(&sun)); // 3 references + venus.details(); + + let earth = Planet::Earth(Rc::clone(&sun)); + println!("reference count = {}", Rc::strong_count(&sun)); // 4 references + earth.details(); + + let mars = Planet::Mars(Rc::clone(&sun)); + println!("reference count = {}", Rc::strong_count(&sun)); // 5 references + mars.details(); + + let jupiter = Planet::Jupiter(Rc::clone(&sun)); + println!("reference count = {}", Rc::strong_count(&sun)); // 6 references + jupiter.details(); + + // TODO + let saturn = Planet::Saturn(Rc::new(Sun)); + println!("reference count = {}", Rc::strong_count(&sun)); // 7 references + saturn.details(); + + // TODO + let uranus = Planet::Uranus(Rc::new(Sun)); + println!("reference count = {}", Rc::strong_count(&sun)); // 8 references + uranus.details(); + + // TODO + let neptune = Planet::Neptune(Rc::new(Sun)); + println!("reference count = {}", Rc::strong_count(&sun)); // 9 references + neptune.details(); + + assert_eq!(Rc::strong_count(&sun), 9); + + drop(neptune); + println!("reference count = {}", Rc::strong_count(&sun)); // 8 references + + drop(uranus); + println!("reference count = {}", Rc::strong_count(&sun)); // 7 references + + drop(saturn); + println!("reference count = {}", Rc::strong_count(&sun)); // 6 references + + drop(jupiter); + println!("reference count = {}", Rc::strong_count(&sun)); // 5 references + + drop(mars); + println!("reference count = {}", Rc::strong_count(&sun)); // 4 references + + // TODO + println!("reference count = {}", Rc::strong_count(&sun)); // 3 references + + // TODO + println!("reference count = {}", Rc::strong_count(&sun)); // 2 references + + // TODO + println!("reference count = {}", Rc::strong_count(&sun)); // 1 reference + + assert_eq!(Rc::strong_count(&sun), 1); + } } diff --git a/exercises/20_threads/threads1.rs b/exercises/20_threads/threads1.rs index 80b6def3..01f9ff44 100644 --- a/exercises/20_threads/threads1.rs +++ b/exercises/20_threads/threads1.rs @@ -1,40 +1,37 @@ -// threads1.rs -// // This program spawns multiple threads that each run for at least 250ms, and // each thread returns how much time they took to complete. The program should // wait until all the spawned threads have finished and should collect their // return values into a vector. -// -// Execute `rustlings hint threads1` or use the `hint` watch subcommand for a -// hint. -// I AM NOT DONE - -use std::thread; -use std::time::{Duration, Instant}; +use std::{ + thread, + time::{Duration, Instant}, +}; fn main() { - let mut handles = vec![]; + let mut handles = Vec::new(); for i in 0..10 { - handles.push(thread::spawn(move || { + let handle = thread::spawn(move || { let start = Instant::now(); thread::sleep(Duration::from_millis(250)); - println!("thread {} is complete", i); + println!("Thread {i} done"); start.elapsed().as_millis() - })); + }); + handles.push(handle); } - let mut results: Vec = vec![]; + let mut results = Vec::new(); for handle in handles { - // TODO: a struct is returned from thread::spawn, can you use it? + // TODO: Collect the results of all threads into the `results` vector. + // Use the `JoinHandle` struct which is returned by `thread::spawn`. } if results.len() != 10 { - panic!("Oh no! All the spawned threads did not finish!"); + panic!("Oh no! Some thread isn't done yet!"); } println!(); for (i, result) in results.into_iter().enumerate() { - println!("thread {} took {}ms", i, result); + println!("Thread {i} took {result}ms"); } } diff --git a/exercises/20_threads/threads2.rs b/exercises/20_threads/threads2.rs index 60d68241..7020cb9c 100644 --- a/exercises/20_threads/threads2.rs +++ b/exercises/20_threads/threads2.rs @@ -1,42 +1,34 @@ -// threads2.rs -// // Building on the last exercise, we want all of the threads to complete their -// work but this time the spawned threads need to be in charge of updating a -// shared value: JobStatus.jobs_completed -// -// Execute `rustlings hint threads2` or use the `hint` watch subcommand for a -// hint. +// work. But this time, the spawned threads need to be in charge of updating a +// shared value: `JobStatus.jobs_done` -// I AM NOT DONE - -use std::sync::Arc; -use std::thread; -use std::time::Duration; +use std::{sync::Arc, thread, time::Duration}; struct JobStatus { - jobs_completed: u32, + jobs_done: u32, } fn main() { - // TODO: `Arc` isn't enough if you want a **mutable** shared state - let status = Arc::new(JobStatus { jobs_completed: 0 }); + // TODO: `Arc` isn't enough if you want a **mutable** shared state. + let status = Arc::new(JobStatus { jobs_done: 0 }); - let mut handles = vec![]; + let mut handles = Vec::new(); for _ in 0..10 { let status_shared = Arc::clone(&status); let handle = thread::spawn(move || { thread::sleep(Duration::from_millis(250)); - // TODO: You must take an action before you update a shared value - status_shared.jobs_completed += 1; + + // TODO: You must take an action before you update a shared value. + status_shared.jobs_done += 1; }); handles.push(handle); } - // Waiting for all jobs to complete + // Waiting for all jobs to complete. for handle in handles { handle.join().unwrap(); } - // TODO: Print the value of `JobStatus.jobs_completed` - println!("Jobs completed: {}", ???); + // TODO: Print the value of `JobStatus.jobs_done`. + println!("Jobs done: {}", todo!()); } diff --git a/exercises/20_threads/threads3.rs b/exercises/20_threads/threads3.rs index acb97b4b..8aa7291f 100644 --- a/exercises/20_threads/threads3.rs +++ b/exercises/20_threads/threads3.rs @@ -1,14 +1,4 @@ -// threads3.rs -// -// Execute `rustlings hint threads3` or use the `hint` watch subcommand for a -// hint. - -// I AM NOT DONE - -use std::sync::mpsc; -use std::sync::Arc; -use std::thread; -use std::time::Duration; +use std::{sync::mpsc, thread, time::Duration}; struct Queue { length: u32, @@ -18,7 +8,7 @@ struct Queue { impl Queue { fn new() -> Self { - Queue { + Self { length: 10, first_half: vec![1, 2, 3, 4, 5], second_half: vec![6, 7, 8, 9, 10], @@ -26,38 +16,49 @@ impl Queue { } } -fn send_tx(q: Queue, tx: mpsc::Sender) -> () { +fn send_tx(q: Queue, tx: mpsc::Sender) { + // TODO: We want to send `tx` to both threads. But currently, it is moved + // into the first thread. How could you solve this problem? thread::spawn(move || { for val in q.first_half { - println!("sending {:?}", val); + println!("Sending {val:?}"); tx.send(val).unwrap(); - thread::sleep(Duration::from_secs(1)); + thread::sleep(Duration::from_millis(250)); } }); thread::spawn(move || { for val in q.second_half { - println!("sending {:?}", val); + println!("Sending {val:?}"); tx.send(val).unwrap(); - thread::sleep(Duration::from_secs(1)); + thread::sleep(Duration::from_millis(250)); } }); } -#[test] fn main() { - let (tx, rx) = mpsc::channel(); - let queue = Queue::new(); - let queue_length = queue.length; - - send_tx(queue, tx); - - let mut total_received: u32 = 0; - for received in rx { - println!("Got: {}", received); - total_received += 1; - } - - println!("total numbers received: {}", total_received); - assert_eq!(total_received, queue_length) + // You can optionally experiment here. +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn threads3() { + let (tx, rx) = mpsc::channel(); + let queue = Queue::new(); + let queue_length = queue.length; + + send_tx(queue, tx); + + let mut total_received: u32 = 0; + for received in rx { + println!("Got: {received}"); + total_received += 1; + } + + println!("Number of received values: {total_received}"); + assert_eq!(total_received, queue_length); + } } diff --git a/exercises/21_macros/macros1.rs b/exercises/21_macros/macros1.rs index 678de6ee..fb3c3ff9 100644 --- a/exercises/21_macros/macros1.rs +++ b/exercises/21_macros/macros1.rs @@ -1,10 +1,3 @@ -// macros1.rs -// -// Execute `rustlings hint macros1` or use the `hint` watch subcommand for a -// hint. - -// I AM NOT DONE - macro_rules! my_macro { () => { println!("Check out my macro!"); @@ -12,5 +5,6 @@ macro_rules! my_macro { } fn main() { + // TODO: Fix the macro call. my_macro(); } diff --git a/exercises/21_macros/macros2.rs b/exercises/21_macros/macros2.rs index 788fc16a..2d9dec76 100644 --- a/exercises/21_macros/macros2.rs +++ b/exercises/21_macros/macros2.rs @@ -1,14 +1,8 @@ -// macros2.rs -// -// Execute `rustlings hint macros2` or use the `hint` watch subcommand for a -// hint. - -// I AM NOT DONE - fn main() { my_macro!(); } +// TODO: Fix the compiler error by moving the whole definition of this macro. macro_rules! my_macro { () => { println!("Check out my macro!"); diff --git a/exercises/21_macros/macros3.rs b/exercises/21_macros/macros3.rs index b795c149..95374948 100644 --- a/exercises/21_macros/macros3.rs +++ b/exercises/21_macros/macros3.rs @@ -1,12 +1,5 @@ -// macros3.rs -// -// Make me compile, without taking the macro out of the module! -// -// Execute `rustlings hint macros3` or use the `hint` watch subcommand for a -// hint. - -// I AM NOT DONE - +// TODO: Fix the compiler error without taking the macro definition out of this +// module. mod macros { macro_rules! my_macro { () => { diff --git a/exercises/21_macros/macros4.rs b/exercises/21_macros/macros4.rs index 71b45a09..9d77f6a5 100644 --- a/exercises/21_macros/macros4.rs +++ b/exercises/21_macros/macros4.rs @@ -1,10 +1,4 @@ -// macros4.rs -// -// Execute `rustlings hint macros4` or use the `hint` watch subcommand for a -// hint. - -// I AM NOT DONE - +// TODO: Fix the compiler error by adding one or two characters. #[rustfmt::skip] macro_rules! my_macro { () => { diff --git a/exercises/22_clippy/clippy1.rs b/exercises/22_clippy/clippy1.rs index e0c6ce7c4..b9d1ec17 100644 --- a/exercises/22_clippy/clippy1.rs +++ b/exercises/22_clippy/clippy1.rs @@ -1,26 +1,17 @@ -// clippy1.rs -// // The Clippy tool is a collection of lints to analyze your code so you can // catch common mistakes and improve your Rust code. // -// For these exercises the code will fail to compile when there are Clippy +// For these exercises, the code will fail to compile when there are Clippy // warnings. Check Clippy's suggestions from the output to solve the exercise. -// -// Execute `rustlings hint clippy1` or use the `hint` watch subcommand for a -// hint. -// I AM NOT DONE - -use std::f32; +use std::f32::consts::PI; fn main() { - let pi = 3.14f32; - let radius = 5.00f32; + // Use the more accurate `PI` constant. + let pi = PI; + let radius: f32 = 5.0; - let area = pi * f32::powi(radius, 2); + let area = pi * radius.powi(2); - println!( - "The area of a circle with radius {:.2} is {:.5}!", - radius, area - ) + println!("The area of a circle with radius {radius:.2} is {area:.5}"); } diff --git a/exercises/22_clippy/clippy2.rs b/exercises/22_clippy/clippy2.rs index 9b87a0b7..8cfe6f80 100644 --- a/exercises/22_clippy/clippy2.rs +++ b/exercises/22_clippy/clippy2.rs @@ -1,15 +1,10 @@ -// clippy2.rs -// -// Execute `rustlings hint clippy2` or use the `hint` watch subcommand for a -// hint. - -// I AM NOT DONE - fn main() { let mut res = 42; let option = Some(12); + // TODO: Fix the Clippy lint. for x in option { res += x; } - println!("{}", res); + + println!("{res}"); } diff --git a/exercises/22_clippy/clippy3.rs b/exercises/22_clippy/clippy3.rs index 5a95f5b8..4f788349 100644 --- a/exercises/22_clippy/clippy3.rs +++ b/exercises/22_clippy/clippy3.rs @@ -1,30 +1,27 @@ -// clippy3.rs -// -// Here's a couple more easy Clippy fixes, so you can see its utility. -// No hints. - -// I AM NOT DONE +// Here are some more easy Clippy fixes so you can see its utility 📎 +// TODO: Fix all the Clippy lints. +#[rustfmt::skip] #[allow(unused_variables, unused_assignments)] fn main() { let my_option: Option<()> = None; if my_option.is_none() { - my_option.unwrap(); + println!("{:?}", my_option.unwrap()); } let my_arr = &[ -1, -2, -3 -4, -5, -6 ]; - println!("My array! Here it is: {:?}", my_arr); + println!("My array! Here it is: {my_arr:?}"); let my_empty_vec = vec![1, 2, 3, 4, 5].resize(0, 5); - println!("This Vec is empty, see? {:?}", my_empty_vec); + println!("This Vec is empty, see? {my_empty_vec:?}"); let mut value_a = 45; let mut value_b = 66; // Let's swap these two! value_a = value_b; value_b = value_a; - println!("value a: {}; value b: {}", value_a, value_b); + println!("value a: {value_a}; value b: {value_b}"); } diff --git a/exercises/23_conversions/as_ref_mut.rs b/exercises/23_conversions/as_ref_mut.rs index 2ba9e3f0..54f0cd11 100644 --- a/exercises/23_conversions/as_ref_mut.rs +++ b/exercises/23_conversions/as_ref_mut.rs @@ -1,31 +1,27 @@ -// as_ref_mut.rs -// // AsRef and AsMut allow for cheap reference-to-reference conversions. Read more // about them at https://doc.rust-lang.org/std/convert/trait.AsRef.html and // https://doc.rust-lang.org/std/convert/trait.AsMut.html, respectively. -// -// Execute `rustlings hint as_ref_mut` or use the `hint` watch subcommand for a -// hint. - -// I AM NOT DONE // Obtain the number of bytes (not characters) in the given argument. -// TODO: Add the AsRef trait appropriately as a trait bound. +// TODO: Add the `AsRef` trait appropriately as a trait bound. fn byte_counter(arg: T) -> usize { arg.as_ref().as_bytes().len() } // Obtain the number of characters (not bytes) in the given argument. -// TODO: Add the AsRef trait appropriately as a trait bound. +// TODO: Add the `AsRef` trait appropriately as a trait bound. fn char_counter(arg: T) -> usize { arg.as_ref().chars().count() } -// Squares a number using as_mut(). +// Squares a number using `as_mut()`. // TODO: Add the appropriate trait bound. fn num_sq(arg: &mut T) { // TODO: Implement the function body. - ??? +} + +fn main() { + // You can optionally experiment here. } #[cfg(test)] diff --git a/exercises/23_conversions/from_into.rs b/exercises/23_conversions/from_into.rs index 10836a6d..bc2783a3 100644 --- a/exercises/23_conversions/from_into.rs +++ b/exercises/23_conversions/from_into.rs @@ -1,89 +1,79 @@ -// from_into.rs -// -// The From trait is used for value-to-value conversions. If From is implemented -// correctly for a type, the Into trait should work conversely. You can read -// more about it at https://doc.rust-lang.org/std/convert/trait.From.html -// -// Execute `rustlings hint from_into` or use the `hint` watch subcommand for a -// hint. +// The `From` trait is used for value-to-value conversions. If `From` is +// implemented, an implementation of `Into` is automatically provided. +// You can read more about it in the documentation: +// https://doc.rust-lang.org/std/convert/trait.From.html #[derive(Debug)] struct Person { name: String, - age: usize, + age: u8, } -// We implement the Default trait to use it as a fallback -// when the provided string is not convertible into a Person object +// We implement the Default trait to use it as a fallback when the provided +// string is not convertible into a `Person` object. impl Default for Person { - fn default() -> Person { - Person { + fn default() -> Self { + Self { name: String::from("John"), age: 30, } } } - -// Your task is to complete this implementation in order for the line `let p1 = -// Person::from("Mark,20")` to compile. Please note that you'll need to parse the -// age component into a `usize` with something like `"4".parse::()`. The -// outcome of this needs to be handled appropriately. +// TODO: Complete this `From` implementation to be able to parse a `Person` +// out of a string in the form of "Mark,20". +// Note that you'll need to parse the age component into a `u8` with something +// like `"4".parse::()`. // // Steps: -// 1. If the length of the provided string is 0, then return the default of -// Person. -// 2. Split the given string on the commas present in it. -// 3. Extract the first element from the split operation and use it as the name. -// 4. If the name is empty, then return the default of Person. -// 5. Extract the other element from the split operation and parse it into a -// `usize` as the age. -// If while parsing the age, something goes wrong, then return the default of -// Person. Otherwise, then return an instantiated Person object with the results - -// I AM NOT DONE - +// 1. Split the given string on the commas present in it. +// 2. If the split operation returns less or more than 2 elements, return the +// default of `Person`. +// 3. Use the first element from the split operation as the name. +// 4. If the name is empty, return the default of `Person`. +// 5. Parse the second element from the split operation into a `u8` as the age. +// 6. If parsing the age fails, return the default of `Person`. impl From<&str> for Person { - fn from(s: &str) -> Person {} + fn from(s: &str) -> Self {} } fn main() { - // Use the `from` function + // Use the `from` function. let p1 = Person::from("Mark,20"); - // Since From is implemented for Person, we should be able to use Into + println!("{p1:?}"); + + // Since `From` is implemented for Person, we are able to use `Into`. let p2: Person = "Gerald,70".into(); - println!("{:?}", p1); - println!("{:?}", p2); + println!("{p2:?}"); } #[cfg(test)] mod tests { use super::*; + #[test] fn test_default() { - // Test that the default person is 30 year old John let dp = Person::default(); assert_eq!(dp.name, "John"); assert_eq!(dp.age, 30); } + #[test] fn test_bad_convert() { - // Test that John is returned when bad string is provided let p = Person::from(""); assert_eq!(p.name, "John"); assert_eq!(p.age, 30); } + #[test] fn test_good_convert() { - // Test that "Mark,20" works let p = Person::from("Mark,20"); assert_eq!(p.name, "Mark"); assert_eq!(p.age, 20); } + #[test] fn test_bad_age() { - // Test that "Mark,twenty" will return the default person due to an - // error in parsing age let p = Person::from("Mark,twenty"); assert_eq!(p.name, "John"); assert_eq!(p.age, 30); diff --git a/exercises/23_conversions/from_str.rs b/exercises/23_conversions/from_str.rs index e2093474..4b1aaa28 100644 --- a/exercises/23_conversions/from_str.rs +++ b/exercises/23_conversions/from_str.rs @@ -1,13 +1,9 @@ -// from_str.rs -// -// This is similar to from_into.rs, but this time we'll implement `FromStr` and -// return errors instead of falling back to a default value. Additionally, upon -// implementing FromStr, you can use the `parse` method on strings to generate -// an object of the implementor type. You can read more about it at +// This is similar to the previous `from_into` exercise. But this time, we'll +// implement `FromStr` and return errors instead of falling back to a default +// value. Additionally, upon implementing `FromStr`, you can use the `parse` +// method on strings to generate an object of the implementor type. You can read +// more about it in the documentation: // https://doc.rust-lang.org/std/str/trait.FromStr.html -// -// Execute `rustlings hint from_str` or use the `hint` watch subcommand for a -// hint. use std::num::ParseIntError; use std::str::FromStr; @@ -15,55 +11,54 @@ use std::str::FromStr; #[derive(Debug, PartialEq)] struct Person { name: String, - age: usize, + age: u8, } // We will use this error type for the `FromStr` implementation. #[derive(Debug, PartialEq)] enum ParsePersonError { - // Empty input string - Empty, // Incorrect number of fields BadLen, // Empty name field NoName, - // Wrapped error from parse::() + // Wrapped error from parse::() ParseInt(ParseIntError), } -// I AM NOT DONE - +// TODO: Complete this `From` implementation to be able to parse a `Person` +// out of a string in the form of "Mark,20". +// Note that you'll need to parse the age component into a `u8` with something +// like `"4".parse::()`. +// // Steps: -// 1. If the length of the provided string is 0, an error should be returned -// 2. Split the given string on the commas present in it -// 3. Only 2 elements should be returned from the split, otherwise return an -// error -// 4. Extract the first element from the split operation and use it as the name -// 5. Extract the other element from the split operation and parse it into a -// `usize` as the age with something like `"4".parse::()` -// 6. If while extracting the name and the age something goes wrong, an error -// should be returned -// If everything goes well, then return a Result of a Person object - +// 1. Split the given string on the commas present in it. +// 2. If the split operation returns less or more than 2 elements, return the +// error `ParsePersonError::BadLen`. +// 3. Use the first element from the split operation as the name. +// 4. If the name is empty, return the error `ParsePersonError::NoName`. +// 5. Parse the second element from the split operation into a `u8` as the age. +// 6. If parsing the age fails, return the error `ParsePersonError::ParseInt`. impl FromStr for Person { type Err = ParsePersonError; - fn from_str(s: &str) -> Result { - } + + fn from_str(s: &str) -> Result {} } fn main() { - let p = "Mark,20".parse::().unwrap(); - println!("{:?}", p); + let p = "Mark,20".parse::(); + println!("{p:?}"); } #[cfg(test)] mod tests { use super::*; + use ParsePersonError::*; #[test] fn empty_input() { - assert_eq!("".parse::(), Err(ParsePersonError::Empty)); + assert_eq!("".parse::(), Err(BadLen)); } + #[test] fn good_input() { let p = "John,32".parse::(); @@ -72,58 +67,47 @@ mod tests { assert_eq!(p.name, "John"); assert_eq!(p.age, 32); } + #[test] fn missing_age() { - assert!(matches!( - "John,".parse::(), - Err(ParsePersonError::ParseInt(_)) - )); + assert!(matches!("John,".parse::(), Err(ParseInt(_)))); } #[test] fn invalid_age() { - assert!(matches!( - "John,twenty".parse::(), - Err(ParsePersonError::ParseInt(_)) - )); + assert!(matches!("John,twenty".parse::(), Err(ParseInt(_)))); } #[test] fn missing_comma_and_age() { - assert_eq!("John".parse::(), Err(ParsePersonError::BadLen)); + assert_eq!("John".parse::(), Err(BadLen)); } #[test] fn missing_name() { - assert_eq!(",1".parse::(), Err(ParsePersonError::NoName)); + assert_eq!(",1".parse::(), Err(NoName)); } #[test] fn missing_name_and_age() { - assert!(matches!( - ",".parse::(), - Err(ParsePersonError::NoName | ParsePersonError::ParseInt(_)) - )); + assert!(matches!(",".parse::(), Err(NoName | ParseInt(_)))); } #[test] fn missing_name_and_invalid_age() { assert!(matches!( ",one".parse::(), - Err(ParsePersonError::NoName | ParsePersonError::ParseInt(_)) + Err(NoName | ParseInt(_)), )); } #[test] fn trailing_comma() { - assert_eq!("John,32,".parse::(), Err(ParsePersonError::BadLen)); + assert_eq!("John,32,".parse::(), Err(BadLen)); } #[test] fn trailing_comma_and_some_string() { - assert_eq!( - "John,32,man".parse::(), - Err(ParsePersonError::BadLen) - ); + assert_eq!("John,32,man".parse::(), Err(BadLen)); } } diff --git a/exercises/23_conversions/try_from_into.rs b/exercises/23_conversions/try_from_into.rs index 32d6ef39..f3ae80a9 100644 --- a/exercises/23_conversions/try_from_into.rs +++ b/exercises/23_conversions/try_from_into.rs @@ -1,14 +1,10 @@ -// try_from_into.rs -// -// TryFrom is a simple and safe type conversion that may fail in a controlled -// way under some circumstances. Basically, this is the same as From. The main -// difference is that this should return a Result type instead of the target -// type itself. You can read more about it at +// `TryFrom` is a simple and safe type conversion that may fail in a controlled +// way under some circumstances. Basically, this is the same as `From`. The main +// difference is that this should return a `Result` type instead of the target +// type itself. You can read more about it in the documentation: // https://doc.rust-lang.org/std/convert/trait.TryFrom.html -// -// Execute `rustlings hint try_from_into` or use the `hint` watch subcommand for -// a hint. +#![allow(clippy::useless_vec)] use std::convert::{TryFrom, TryInto}; #[derive(Debug, PartialEq)] @@ -18,7 +14,7 @@ struct Color { blue: u8, } -// We will use this error type for these `TryFrom` conversions. +// We will use this error type for the `TryFrom` conversions. #[derive(Debug, PartialEq)] enum IntoColorError { // Incorrect length of slice @@ -27,80 +23,67 @@ enum IntoColorError { IntConversion, } -// I AM NOT DONE - -// Your task is to complete this implementation and return an Ok result of inner -// type Color. You need to create an implementation for a tuple of three -// integers, an array of three integers, and a slice of integers. -// -// Note that the implementation for tuple and array will be checked at compile -// time, but the slice implementation needs to check the slice length! Also note -// that correct RGB color values must be integers in the 0..=255 range. - -// Tuple implementation +// TODO: Tuple implementation. +// Correct RGB color values must be integers in the 0..=255 range. impl TryFrom<(i16, i16, i16)> for Color { type Error = IntoColorError; - fn try_from(tuple: (i16, i16, i16)) -> Result { - } + + fn try_from(tuple: (i16, i16, i16)) -> Result {} } -// Array implementation +// TODO: Array implementation. impl TryFrom<[i16; 3]> for Color { type Error = IntoColorError; - fn try_from(arr: [i16; 3]) -> Result { - } + + fn try_from(arr: [i16; 3]) -> Result {} } -// Slice implementation +// TODO: Slice implementation. +// This implementation needs to check the slice length. impl TryFrom<&[i16]> for Color { type Error = IntoColorError; - fn try_from(slice: &[i16]) -> Result { - } + + fn try_from(slice: &[i16]) -> Result {} } fn main() { - // Use the `try_from` function + // Using the `try_from` function. let c1 = Color::try_from((183, 65, 14)); - println!("{:?}", c1); + println!("{c1:?}"); - // Since TryFrom is implemented for Color, we should be able to use TryInto + // Since `TryFrom` is implemented for `Color`, we can use `TryInto`. let c2: Result = [183, 65, 14].try_into(); - println!("{:?}", c2); + println!("{c2:?}"); let v = vec![183, 65, 14]; - // With slice we should use `try_from` function + // With slice we should use the `try_from` function let c3 = Color::try_from(&v[..]); - println!("{:?}", c3); - // or take slice within round brackets and use TryInto + println!("{c3:?}"); + // or put the slice within round brackets and use `try_into`. let c4: Result = (&v[..]).try_into(); - println!("{:?}", c4); + println!("{c4:?}"); } #[cfg(test)] mod tests { use super::*; + use IntoColorError::*; #[test] fn test_tuple_out_of_range_positive() { - assert_eq!( - Color::try_from((256, 1000, 10000)), - Err(IntoColorError::IntConversion) - ); + assert_eq!(Color::try_from((256, 1000, 10000)), Err(IntConversion)); } + #[test] fn test_tuple_out_of_range_negative() { - assert_eq!( - Color::try_from((-1, -10, -256)), - Err(IntoColorError::IntConversion) - ); + assert_eq!(Color::try_from((-1, -10, -256)), Err(IntConversion)); } + #[test] fn test_tuple_sum() { - assert_eq!( - Color::try_from((-1, 255, 255)), - Err(IntoColorError::IntConversion) - ); + assert_eq!(Color::try_from((-1, 255, 255)), Err(IntConversion)); } + #[test] fn test_tuple_correct() { let c: Result = (183, 65, 14).try_into(); @@ -110,25 +93,29 @@ mod tests { Color { red: 183, green: 65, - blue: 14 + blue: 14, } ); } + #[test] fn test_array_out_of_range_positive() { let c: Result = [1000, 10000, 256].try_into(); - assert_eq!(c, Err(IntoColorError::IntConversion)); + assert_eq!(c, Err(IntConversion)); } + #[test] fn test_array_out_of_range_negative() { let c: Result = [-10, -256, -1].try_into(); - assert_eq!(c, Err(IntoColorError::IntConversion)); + assert_eq!(c, Err(IntConversion)); } + #[test] fn test_array_sum() { let c: Result = [-1, 255, 255].try_into(); - assert_eq!(c, Err(IntoColorError::IntConversion)); + assert_eq!(c, Err(IntConversion)); } + #[test] fn test_array_correct() { let c: Result = [183, 65, 14].try_into(); @@ -142,30 +129,25 @@ mod tests { } ); } + #[test] fn test_slice_out_of_range_positive() { let arr = [10000, 256, 1000]; - assert_eq!( - Color::try_from(&arr[..]), - Err(IntoColorError::IntConversion) - ); + assert_eq!(Color::try_from(&arr[..]), Err(IntConversion)); } + #[test] fn test_slice_out_of_range_negative() { let arr = [-256, -1, -10]; - assert_eq!( - Color::try_from(&arr[..]), - Err(IntoColorError::IntConversion) - ); + assert_eq!(Color::try_from(&arr[..]), Err(IntConversion)); } + #[test] fn test_slice_sum() { let arr = [-1, 255, 255]; - assert_eq!( - Color::try_from(&arr[..]), - Err(IntoColorError::IntConversion) - ); + assert_eq!(Color::try_from(&arr[..]), Err(IntConversion)); } + #[test] fn test_slice_correct() { let v = vec![183, 65, 14]; @@ -176,18 +158,20 @@ mod tests { Color { red: 183, green: 65, - blue: 14 + blue: 14, } ); } + #[test] fn test_slice_excess_length() { let v = vec![0, 0, 0, 0]; - assert_eq!(Color::try_from(&v[..]), Err(IntoColorError::BadLen)); + assert_eq!(Color::try_from(&v[..]), Err(BadLen)); } + #[test] fn test_slice_insufficient_length() { let v = vec![0, 0]; - assert_eq!(Color::try_from(&v[..]), Err(IntoColorError::BadLen)); + assert_eq!(Color::try_from(&v[..]), Err(BadLen)); } } diff --git a/exercises/23_conversions/using_as.rs b/exercises/23_conversions/using_as.rs index 414cef3a..c131d1f3 100644 --- a/exercises/23_conversions/using_as.rs +++ b/exercises/23_conversions/using_as.rs @@ -1,19 +1,10 @@ -// using_as.rs -// -// Type casting in Rust is done via the usage of the `as` operator. Please note -// that the `as` operator is not only used when type casting. It also helps with -// renaming imports. -// -// The goal is to make sure that the division does not fail to compile and -// returns the proper type. -// -// Execute `rustlings hint using_as` or use the `hint` watch subcommand for a -// hint. - -// I AM NOT DONE +// Type casting in Rust is done via the usage of the `as` operator. +// Note that the `as` operator is not only used when type casting. It also helps +// with renaming imports. fn average(values: &[f64]) -> f64 { let total = values.iter().sum::(); + // TODO: Make a conversion before dividing. total / values.len() } diff --git a/exercises/quiz1.rs b/exercises/quiz1.rs deleted file mode 100644 index 4ee5ada7..00000000 --- a/exercises/quiz1.rs +++ /dev/null @@ -1,33 +0,0 @@ -// quiz1.rs -// -// This is a quiz for the following sections: -// - Variables -// - Functions -// - If -// -// Mary is buying apples. The price of an apple is calculated as follows: -// - An apple costs 2 rustbucks. -// - If Mary buys more than 40 apples, each apple only costs 1 rustbuck! -// Write a function that calculates the price of an order of apples given the -// quantity bought. -// -// No hints this time ;) - -// I AM NOT DONE - -// Put your function here! -// fn calculate_price_of_apples { - -// Don't modify this function! -#[test] -fn verify_test() { - let price1 = calculate_price_of_apples(35); - let price2 = calculate_price_of_apples(40); - let price3 = calculate_price_of_apples(41); - let price4 = calculate_price_of_apples(65); - - assert_eq!(70, price1); - assert_eq!(80, price2); - assert_eq!(41, price3); - assert_eq!(65, price4); -} diff --git a/exercises/quiz2.rs b/exercises/quiz2.rs deleted file mode 100644 index f9ba953c..00000000 --- a/exercises/quiz2.rs +++ /dev/null @@ -1,64 +0,0 @@ -// quiz2.rs -// -// This is a quiz for the following sections: -// - Strings -// - Vecs -// - Move semantics -// - Modules -// - Enums -// -// Let's build a little machine in the form of a function. As input, we're going -// to give a list of strings and commands. These commands determine what action -// is going to be applied to the string. It can either be: -// - Uppercase the string -// - Trim the string -// - Append "bar" to the string a specified amount of times -// The exact form of this will be: -// - The input is going to be a Vector of 2-length tuples, -// the first element is the string, the second one is the command. -// - The output element is going to be a Vector of strings. -// -// No hints this time! - -// I AM NOT DONE - -pub enum Command { - Uppercase, - Trim, - Append(usize), -} - -mod my_module { - use super::Command; - - // TODO: Complete the function signature! - pub fn transformer(input: ???) -> ??? { - // TODO: Complete the output declaration! - let mut output: ??? = vec![]; - for (string, command) in input.iter() { - // TODO: Complete the function body. You can do it! - } - output - } -} - -#[cfg(test)] -mod tests { - // TODO: What do we need to import to have `transformer` in scope? - use ???; - use super::Command; - - #[test] - fn it_works() { - let output = transformer(vec![ - ("hello".into(), Command::Uppercase), - (" all roads lead to rome! ".into(), Command::Trim), - ("foo".into(), Command::Append(1)), - ("bar".into(), Command::Append(5)), - ]); - assert_eq!(output[0], "HELLO"); - assert_eq!(output[1], "all roads lead to rome!"); - assert_eq!(output[2], "foobar"); - assert_eq!(output[3], "barbarbarbarbarbar"); - } -} diff --git a/exercises/quizzes/README.md b/exercises/quizzes/README.md new file mode 100644 index 00000000..4d3bcd94 --- /dev/null +++ b/exercises/quizzes/README.md @@ -0,0 +1,3 @@ +# Quizzes + +After every couple of sections, there will be a quiz in this directory that'll test your knowledge on a bunch of sections at once. diff --git a/exercises/quizzes/quiz1.rs b/exercises/quizzes/quiz1.rs new file mode 100644 index 00000000..5f17514b --- /dev/null +++ b/exercises/quizzes/quiz1.rs @@ -0,0 +1,31 @@ +// This is a quiz for the following sections: +// - Variables +// - Functions +// - If +// +// Mary is buying apples. The price of an apple is calculated as follows: +// - An apple costs 2 rustbucks. +// - If Mary buys more than 40 apples, each apple only costs 1 rustbuck! +// Write a function that calculates the price of an order of apples given the +// quantity bought. + +// Put your function here! +// fn calculate_price_of_apples(???) -> ??? { + +fn main() { + // You can optionally experiment here. +} + +// Don't change the tests! +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn verify_test() { + assert_eq!(calculate_price_of_apples(35), 70); + assert_eq!(calculate_price_of_apples(40), 80); + assert_eq!(calculate_price_of_apples(41), 41); + assert_eq!(calculate_price_of_apples(65), 65); + } +} diff --git a/exercises/quizzes/quiz2.rs b/exercises/quizzes/quiz2.rs new file mode 100644 index 00000000..8ef1342a --- /dev/null +++ b/exercises/quizzes/quiz2.rs @@ -0,0 +1,63 @@ +// This is a quiz for the following sections: +// - Strings +// - Vecs +// - Move semantics +// - Modules +// - Enums +// +// Let's build a little machine in the form of a function. As input, we're going +// to give a list of strings and commands. These commands determine what action +// is going to be applied to the string. It can either be: +// - Uppercase the string +// - Trim the string +// - Append "bar" to the string a specified amount of times +// +// The exact form of this will be: +// - The input is going to be a Vector of 2-length tuples, +// the first element is the string, the second one is the command. +// - The output element is going to be a vector of strings. + +enum Command { + Uppercase, + Trim, + Append(usize), +} + +mod my_module { + use super::Command; + + // TODO: Complete the function. + // pub fn transformer(input: ???) -> ??? { ??? } +} + +fn main() { + // You can optionally experiment here. +} + +#[cfg(test)] +mod tests { + // TODO: What do we need to import to have `transformer` in scope? + // use ???; + use super::Command; + + #[test] + fn it_works() { + let input = vec![ + ("hello".to_string(), Command::Uppercase), + (" all roads lead to rome! ".to_string(), Command::Trim), + ("foo".to_string(), Command::Append(1)), + ("bar".to_string(), Command::Append(5)), + ]; + let output = transformer(input); + + assert_eq!( + output, + [ + "HELLO", + "all roads lead to rome!", + "foobar", + "barbarbarbarbarbar", + ] + ); + } +} diff --git a/exercises/quiz3.rs b/exercises/quizzes/quiz3.rs similarity index 51% rename from exercises/quiz3.rs rename to exercises/quizzes/quiz3.rs index 3b01d313..c877c5f8 100644 --- a/exercises/quiz3.rs +++ b/exercises/quizzes/quiz3.rs @@ -1,36 +1,37 @@ -// quiz3.rs -// // This quiz tests: // - Generics // - Traits // // An imaginary magical school has a new report card generation system written -// in Rust! Currently the system only supports creating report cards where the +// in Rust! Currently, the system only supports creating report cards where the // student's grade is represented numerically (e.g. 1.0 -> 5.5). However, the // school also issues alphabetical grades (A+ -> F-) and needs to be able to // print both types of report card! // -// Make the necessary code changes in the struct ReportCard and the impl block -// to support alphabetical report cards. Change the Grade in the second test to -// "A+" to show that your changes allow alphabetical grades. -// -// Execute `rustlings hint quiz3` or use the `hint` watch subcommand for a hint. +// Make the necessary code changes in the struct `ReportCard` and the impl +// block to support alphabetical report cards in addition to numerical ones. -// I AM NOT DONE - -pub struct ReportCard { - pub grade: f32, - pub student_name: String, - pub student_age: u8, +// TODO: Adjust the struct as described above. +struct ReportCard { + grade: f32, + student_name: String, + student_age: u8, } +// TODO: Adjust the impl block as described above. impl ReportCard { - pub fn print(&self) -> String { - format!("{} ({}) - achieved a grade of {}", - &self.student_name, &self.student_age, &self.grade) + fn print(&self) -> String { + format!( + "{} ({}) - achieved a grade of {}", + &self.student_name, &self.student_age, &self.grade, + ) } } +fn main() { + // You can optionally experiment here. +} + #[cfg(test)] mod tests { use super::*; @@ -44,21 +45,20 @@ mod tests { }; assert_eq!( report_card.print(), - "Tom Wriggle (12) - achieved a grade of 2.1" + "Tom Wriggle (12) - achieved a grade of 2.1", ); } #[test] fn generate_alphabetic_report_card() { - // TODO: Make sure to change the grade here after you finish the exercise. let report_card = ReportCard { - grade: 2.1, + grade: "A+", student_name: "Gary Plotter".to_string(), student_age: 11, }; assert_eq!( report_card.print(), - "Gary Plotter (11) - achieved a grade of A+" + "Gary Plotter (11) - achieved a grade of A+", ); } } diff --git a/flake.lock b/flake.lock deleted file mode 100644 index 6592dd89..00000000 --- a/flake.lock +++ /dev/null @@ -1,78 +0,0 @@ -{ - "nodes": { - "flake-compat": { - "flake": false, - "locked": { - "lastModified": 1696426674, - "narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=", - "owner": "edolstra", - "repo": "flake-compat", - "rev": "0f9255e01c2351cc7d116c072cb317785dd33b33", - "type": "github" - }, - "original": { - "owner": "edolstra", - "repo": "flake-compat", - "type": "github" - } - }, - "flake-utils": { - "inputs": { - "systems": "systems" - }, - "locked": { - "lastModified": 1710146030, - "narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=", - "owner": "numtide", - "repo": "flake-utils", - "rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a", - "type": "github" - }, - "original": { - "owner": "numtide", - "repo": "flake-utils", - "type": "github" - } - }, - "nixpkgs": { - "locked": { - "lastModified": 1715447595, - "narHash": "sha256-VsVAUQOj/cS1LCOmMjAGeRksXIAdPnFIjCQ0XLkCsT0=", - "owner": "nixos", - "repo": "nixpkgs", - "rev": "062ca2a9370a27a35c524dc82d540e6e9824b652", - "type": "github" - }, - "original": { - "owner": "nixos", - "ref": "nixos-unstable", - "repo": "nixpkgs", - "type": "github" - } - }, - "root": { - "inputs": { - "flake-compat": "flake-compat", - "flake-utils": "flake-utils", - "nixpkgs": "nixpkgs" - } - }, - "systems": { - "locked": { - "lastModified": 1681028828, - "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", - "owner": "nix-systems", - "repo": "default", - "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", - "type": "github" - }, - "original": { - "owner": "nix-systems", - "repo": "default", - "type": "github" - } - } - }, - "root": "root", - "version": 7 -} diff --git a/flake.nix b/flake.nix deleted file mode 100644 index 152d38e6..00000000 --- a/flake.nix +++ /dev/null @@ -1,78 +0,0 @@ -{ - description = "Small exercises to get you used to reading and writing Rust code"; - - inputs = { - flake-compat = { - url = "github:edolstra/flake-compat"; - flake = false; - }; - flake-utils.url = "github:numtide/flake-utils"; - nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; - }; - - outputs = { self, flake-utils, nixpkgs, ... }: - flake-utils.lib.eachDefaultSystem (system: - let - pkgs = nixpkgs.legacyPackages.${system}; - - cargoBuildInputs = with pkgs; lib.optionals stdenv.isDarwin [ - darwin.apple_sdk.frameworks.CoreServices - ]; - - rustlings = - pkgs.rustPlatform.buildRustPackage { - name = "rustlings"; - version = "5.6.1"; - - buildInputs = cargoBuildInputs; - nativeBuildInputs = [pkgs.git]; - - src = with pkgs.lib; cleanSourceWith { - src = self; - # a function that returns a bool determining if the path should be included in the cleaned source - filter = path: type: - let - # filename - baseName = builtins.baseNameOf (toString path); - # path from root directory - path' = builtins.replaceStrings [ "${self}/" ] [ "" ] path; - # checks if path is in the directory - inDirectory = directory: hasPrefix directory path'; - in - inDirectory "src" || - inDirectory "tests" || - hasPrefix "Cargo" baseName || - baseName == "info.toml"; - }; - - cargoLock.lockFile = ./Cargo.lock; - }; - in - { - devShell = pkgs.mkShell { - RUST_SRC_PATH = "${pkgs.rust.packages.stable.rustPlatform.rustLibSrc}"; - - buildInputs = with pkgs; [ - cargo - rustc - rust-analyzer - rustlings - rustfmt - clippy - ] ++ cargoBuildInputs; - }; - apps = let - rustlings-app = { - type = "app"; - program = "${rustlings}/bin/rustlings"; - }; - in { - default = rustlings-app; - rustlings = rustlings-app; - }; - packages = { - inherit rustlings; - default = rustlings; - }; - }); -} diff --git a/install.ps1 b/install.ps1 deleted file mode 100644 index 8ab5b88e..00000000 --- a/install.ps1 +++ /dev/null @@ -1,94 +0,0 @@ -#!/usr/bin/env pwsh - -#Requires -Version 5 -param($path = "$home/rustlings") - -Write-Host "Let's get you set up with Rustlings!" - -Write-Host "Checking requirements..." -if (Get-Command git -ErrorAction SilentlyContinue) { - Write-Host "SUCCESS: Git is installed" -} else { - Write-Host "WARNING: Git does not seem to be installed." - Write-Host "Please download Git using your package manager or over https://git-scm.com/!" - exit 1 -} - -if (Get-Command rustc -ErrorAction SilentlyContinue) { - Write-Host "SUCCESS: Rust is installed" -} else { - Write-Host "WARNING: Rust does not seem to be installed." - Write-Host "Please download Rust using https://rustup.rs!" - exit 1 -} - -if (Get-Command cargo -ErrorAction SilentlyContinue) { - Write-Host "SUCCESS: Cargo is installed" -} else { - Write-Host "WARNING: Cargo does not seem to be installed." - Write-Host "Please download Rust and Cargo using https://rustup.rs!" - exit 1 -} - -# Function that compares two versions strings v1 and v2 given in arguments (e.g 1.31 and 1.33.0). -# Returns 1 if v1 > v2, 0 if v1 == v2, 2 if v1 < v2. -function vercomp($v1, $v2) { - if ($v1 -eq $v2) { - return 0 - } - - $v1 = $v1.Replace(".", "0") - $v2 = $v2.Replace(".", "0") - if ($v1.Length -gt $v2.Length) { - $v2 = $v2.PadRight($v1.Length, "0") - } else { - $v1 = $v1.PadRight($v2.Length, "0") - } - - if ($v1 -gt $v2) { - return 1 - } else { - return 2 - } -} - -$rustVersion = $(rustc --version).Split(" ")[1] -$minRustVersion = "1.70" -if ((vercomp $rustVersion $minRustVersion) -eq 2) { - Write-Host "WARNING: Rust version is too old: $rustVersion - needs at least $minRustVersion" - Write-Host "Please update Rust with 'rustup update'" - exit 1 -} else { - Write-Host "SUCCESS: Rust is up to date" -} - -Write-Host "Cloning Rustlings at $path" -git clone -q https://github.com/rust-lang/rustlings $path -if (!($LASTEXITCODE -eq 0)) { - exit 1 -} - -# UseBasicParsing is deprecated, pwsh 6 or above will automatically use it, -# but anyone running pwsh 5 will have to pass the argument. -$version = Invoke-WebRequest -UseBasicParsing https://api.github.com/repos/rust-lang/rustlings/releases/latest ` - | ConvertFrom-Json | Select-Object -ExpandProperty tag_name - -Write-Host "Checking out version $version..." -Set-Location $path -git checkout -q tags/$version - -Write-Host "Installing the 'rustlings' executable..." -cargo install --locked --force --path . -if (!(Get-Command rustlings -ErrorAction SilentlyContinue)) { - Write-Host "WARNING: Please check that you have '~/.cargo/bin' in your PATH environment variable!" -} - -# Checking whether Clippy is installed. -# Due to a bug in Cargo, this must be done with Rustup: https://github.com/rust-lang/rustup/issues/1514 -$clippy = (rustup component list | Select-String "clippy" | Select-String "installed") | Out-String -if (!$clippy) { - Write-Host "Installing the 'cargo-clippy' executable..." - rustup component add clippy -} - -Write-Host "All done! Navigate to $path and run 'rustlings' to get started!" diff --git a/install.sh b/install.sh deleted file mode 100755 index f6031cf8..00000000 --- a/install.sh +++ /dev/null @@ -1,184 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -echo -e "\nLet's get you set up with Rustlings!" - -echo "Checking requirements..." -if [ -x "$(command -v git)" ] -then - echo "SUCCESS: Git is installed" -else - echo "ERROR: Git does not seem to be installed." - echo "Please download Git using your package manager or over https://git-scm.com/!" - exit 1 -fi - -if [ -x "$(command -v cc)" ] -then - echo "SUCCESS: cc is installed" -else - echo "ERROR: cc does not seem to be installed." - echo "Please download (g)cc using your package manager." - echo "OSX: xcode-select --install" - echo "Deb: sudo apt install gcc" - echo "Yum: sudo yum -y install gcc" - exit 1 -fi - -if [ -x "$(command -v rustup)" ] -then - echo "SUCCESS: rustup is installed" -else - echo "ERROR: rustup does not seem to be installed." - echo "Please download rustup using https://rustup.rs!" - exit 1 -fi - -if [ -x "$(command -v rustc)" ] -then - echo "SUCCESS: Rust is installed" -else - echo "ERROR: Rust does not seem to be installed." - echo "Please download Rust using rustup!" - exit 1 -fi - -if [ -x "$(command -v cargo)" ] -then - echo "SUCCESS: Cargo is installed" -else - echo "ERROR: Cargo does not seem to be installed." - echo "Please download Rust and Cargo using rustup!" - exit 1 -fi - -# Look up python installations, starting with 3 with a fallback of 2 -if [ -x "$(command -v python3)" ] -then - PY="$(command -v python3)" -elif [ -x "$(command -v python)" ] -then - PY="$(command -v python)" -elif [ -x "$(command -v python2)" ] -then - PY="$(command -v python2)" -else - echo "ERROR: No working python installation was found" - echo "Please install python and add it to the PATH variable" - exit 1 -fi - -# Function that compares two versions strings v1 and v2 given in arguments (e.g 1.31 and 1.33.0). -# Returns 1 if v1 > v2, 0 if v1 == v2, 2 if v1 < v2. -function vercomp() { - if [[ $1 == $2 ]] - then - return 0 - fi - v1=( ${1//./ } ) - v2=( ${2//./ } ) - len1=${#v1[@]} - len2=${#v2[@]} - max_len=$len1 - if [[ $max_len -lt $len2 ]] - then - max_len=$len2 - fi - - #pad right in short arr - if [[ len1 -gt len2 ]]; - then - for ((i = len2; i < len1; i++)); - do - v2[$i]=0 - done - else - for ((i = len1; i < len2; i++)); - do - v1[$i]=0 - done - fi - - for i in `seq 0 $((max_len-1))` - do - # Fill empty fields with zeros in v1 - if [ -z "${v1[$i]}" ] - then - v1[$i]=0 - fi - # And in v2 - if [ -z "${v2[$i]}" ] - then - v2[$i]=0 - fi - if [ ${v1[$i]} -gt ${v2[$i]} ] - then - return 1 - fi - if [ ${v1[$i]} -lt ${v2[$i]} ] - then - return 2 - fi - done - return 0 -} - -RustVersion=$(rustc --version | cut -d " " -f 2) -MinRustVersion=1.70 -vercomp "$RustVersion" $MinRustVersion || ec=$? -if [ ${ec:-0} -eq 2 ] -then - echo "ERROR: Rust version is too old: $RustVersion - needs at least $MinRustVersion" - echo "Please update Rust with 'rustup update'" - exit 1 -else - echo "SUCCESS: Rust is up to date" -fi - -Path=${1:-rustlings/} -echo "Cloning Rustlings at $Path..." -git clone -q https://github.com/rust-lang/rustlings.git "$Path" - -cd "$Path" - -Version=$(curl -s https://api.github.com/repos/rust-lang/rustlings/releases/latest | ${PY} -c "import json,sys;obj=json.load(sys.stdin);print(obj['tag_name']) if 'tag_name' in obj else sys.exit(f\"Error: {obj['message']}\");") -CargoBin="${CARGO_HOME:-$HOME/.cargo}/bin" - -if [[ -z ${Version} ]] -then - echo "The latest tag version could not be fetched remotely." - echo "Using the local git repository..." - Version=$(ls -tr .git/refs/tags/ | tail -1) - if [[ -z ${Version} ]] - then - echo "No valid tag version found" - echo "Rustlings will be installed using the main branch" - Version="main" - else - Version="tags/${Version}" - fi -else - Version="tags/${Version}" -fi - -echo "Checking out version $Version..." -git checkout -q ${Version} - -echo "Installing the 'rustlings' executable..." -cargo install --locked --force --path . - -if ! [ -x "$(command -v rustlings)" ] -then - echo "WARNING: Please check that you have '$CargoBin' in your PATH environment variable!" -fi - -# Checking whether Clippy is installed. -# Due to a bug in Cargo, this must be done with Rustup: https://github.com/rust-lang/rustup/issues/1514 -Clippy=$(rustup component list | grep "clippy" | grep "installed") -if [ -z "$Clippy" ] -then - echo "Installing the 'cargo-clippy' executable..." - rustup component add clippy -fi - -echo "All done! Run 'rustlings' to get started." diff --git a/release-hook.sh b/release-hook.sh new file mode 100755 index 00000000..052832f2 --- /dev/null +++ b/release-hook.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +# Error out if any command fails +set -e + +cargo run -- dev check +typos +cargo outdated -w --exit-code 1 +cargo test --workspace --all-targets diff --git a/rustlings-macros/Cargo.toml b/rustlings-macros/Cargo.toml new file mode 100644 index 00000000..20d6776e --- /dev/null +++ b/rustlings-macros/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "rustlings-macros" +description = "A macros crate intended to be used only by Rustlings" +version.workspace = true +authors.workspace = true +repository.workspace = true +license.workspace = true +edition.workspace = true +include = [ + "/src/", + "/info.toml", +] + +[lib] +proc-macro = true + +[dependencies] +quote = "1.0.36" +serde.workspace = true +toml_edit.workspace = true diff --git a/info.toml b/rustlings-macros/info.toml similarity index 53% rename from info.toml rename to rustlings-macros/info.toml index ba9b6e3d..488fdacf 100644 --- a/info.toml +++ b/rustlings-macros/info.toml @@ -1,39 +1,69 @@ +format_version = 1 + +welcome_message = """Is this your first time? Don't worry, Rustlings is made for beginners! +We are going to teach you a lot of things about Rust, but before we can +get started, here are some notes about how Rustlings operates: + +1. The central concept behind Rustlings is that you solve exercises. These + exercises usually contain some compiler or logic errors which cause the + exercise to fail compilation or testing. It's your job to find all errors + and fix them! +2. Make sure to have your editor open in the `rustlings/` directory. Rustlings + will show you the path of the current exercise under the progress bar. Open + the exercise file in your editor, fix errors and save the file. Rustlings will + automatically detect the file change and rerun the exercise. If all errors are + fixed, Rustlings will ask you to move on to the next exercise. +3. If you're stuck on an exercise, enter `h` to show a hint. +4. If an exercise doesn't make sense to you, feel free to open an issue on GitHub! + (https://github.com/rust-lang/rustlings). We look at every issue, and sometimes, + other learners do too so you can help each other out! +""" + +final_message = """We hope you enjoyed learning about the various aspects of Rust! +If you noticed any issues, don't hesitate to report them on Github. +You can also contribute your own exercises to help the greater community! + +Before reporting an issue or contributing, please read our guidelines: +https://github.com/rust-lang/rustlings/blob/main/CONTRIBUTING.md +""" + # INTRO [[exercises]] name = "intro1" -path = "exercises/00_intro/intro1.rs" -mode = "compile" +dir = "00_intro" +test = false hint = """ -Remove the `I AM NOT DONE` comment in the `exercises/intro00/intro1.rs` file -to move on to the next exercise.""" +Enter `n` to move on to the next exercise. +You might need to press ENTER after typing `n`.""" [[exercises]] name = "intro2" -path = "exercises/00_intro/intro2.rs" -mode = "compile" +dir = "00_intro" +test = false hint = """ -The compiler is informing us that we've got the name of the print macro wrong, and has suggested an alternative.""" +The compiler is informing us that we've got the name of the print macro wrong. +It also suggests an alternative.""" # VARIABLES [[exercises]] name = "variables1" -path = "exercises/01_variables/variables1.rs" -mode = "compile" +dir = "01_variables" +test = false hint = """ -The declaration in the first line in the main function is missing a keyword -that is needed in Rust to create a new variable binding.""" +The declaration in the `main` function is missing a keyword that is needed +in Rust to create a new variable binding.""" [[exercises]] name = "variables2" -path = "exercises/01_variables/variables2.rs" -mode = "compile" +dir = "01_variables" +test = false hint = """ -The compiler message is saying that Rust cannot infer the type that the +The compiler message is saying that Rust can't infer the type that the variable binding `x` has with what is given here. -What happens if you annotate the first line in the main function with a type +What happens if you annotate the first line in the `main` function with a type annotation? What if you give `x` a value? @@ -46,12 +76,12 @@ What if `x` is the same type as `10`? What if it's a different type?""" [[exercises]] name = "variables3" -path = "exercises/01_variables/variables3.rs" -mode = "compile" +dir = "01_variables" +test = false hint = """ -Oops! In this exercise, we have a variable binding that we've created on in the -first line in the `main` function, and we're trying to use it in the next line, -but we haven't given it a value. +In this exercise, we have a variable binding that we've created in the `main` +function, and we're trying to use it in the next line, but we haven't given it +a value. We can't print out something that isn't there; try giving `x` a value! @@ -60,17 +90,17 @@ programming language -- thankfully the Rust compiler has caught this for us!""" [[exercises]] name = "variables4" -path = "exercises/01_variables/variables4.rs" -mode = "compile" +dir = "01_variables" +test = false hint = """ -In Rust, variable bindings are immutable by default. But here we're trying +In Rust, variable bindings are immutable by default. But here, we're trying to reassign a different value to `x`! There's a keyword we can use to make a variable binding mutable instead.""" [[exercises]] name = "variables5" -path = "exercises/01_variables/variables5.rs" -mode = "compile" +dir = "01_variables" +test = false hint = """ In `variables4` we already learned how to make an immutable variable mutable using a special keyword. Unfortunately this doesn't help us much in this @@ -87,16 +117,16 @@ Try to solve this exercise afterwards using this technique.""" [[exercises]] name = "variables6" -path = "exercises/01_variables/variables6.rs" -mode = "compile" +dir = "01_variables" +test = false hint = """ We know about variables and mutability, but there is another important type of -variable available: constants. +variables available: constants. -Constants are always immutable and they are declared with keyword `const` rather -than keyword `let`. +Constants are always immutable. They are declared with the keyword `const` instead +of `let`. -Constants types must also always be annotated. +The type of Constants must always be annotated. Read more about constants and the differences between variables and constants under 'Constants' in the book's section 'Variables and Mutability': @@ -107,66 +137,61 @@ https://doc.rust-lang.org/book/ch03-01-variables-and-mutability.html#constants [[exercises]] name = "functions1" -path = "exercises/02_functions/functions1.rs" -mode = "compile" +dir = "02_functions" +test = false hint = """ -This main function is calling a function that it expects to exist, but the +This `main` function is calling a function that it expects to exist, but the function doesn't exist. It expects this function to have the name `call_me`. -It expects this function to not take any arguments and not return a value. +It also expects this function to not take any arguments and not return a value. Sounds a lot like `main`, doesn't it?""" [[exercises]] name = "functions2" -path = "exercises/02_functions/functions2.rs" -mode = "compile" +dir = "02_functions" +test = false hint = """ Rust requires that all parts of a function's signature have type annotations, but `call_me` is missing the type annotation of `num`.""" [[exercises]] name = "functions3" -path = "exercises/02_functions/functions3.rs" -mode = "compile" +dir = "02_functions" +test = false hint = """ This time, the function *declaration* is okay, but there's something wrong -with the place where we're calling the function. - -As a reminder, you can freely play around with different solutions in Rustlings! -Watch mode will only jump to the next exercise if you remove the `I AM NOT -DONE` comment.""" +with the place where we are calling the function.""" [[exercises]] name = "functions4" -path = "exercises/02_functions/functions4.rs" -mode = "compile" +dir = "02_functions" +test = false hint = """ The error message points to the function `sale_price` and says it expects a type -after the `->`. This is where the function's return type should be -- take a -look at the `is_even` function for an example!""" +after `->`. This is where the function's return type should be. +Take a look at the `is_even` function for an example!""" [[exercises]] name = "functions5" -path = "exercises/02_functions/functions5.rs" -mode = "compile" +dir = "02_functions" +test = false hint = """ This is a really common error that can be fixed by removing one character. It happens because Rust distinguishes between expressions and statements: -expressions return a value based on their operand(s), and statements simply -return a `()` type which behaves just like `void` in C/C++ language. +Expressions return a value based on their operand(s), and statements simply +return a `()` type which behaves just like `void` in C/C++. -We want to return a value of `i32` type from the `square` function, but it is -returning a `()` type... +We want to return a value with the type `i32` from the `square` function, but +it is returning the type `()`. -They are not the same. There are two solutions: -1. Add a `return` ahead of `num * num;` -2. remove `;`, make it to be `num * num`""" +There are two solutions: +1. Add the `return` keyword before `num * num;` +2. Remove the semicolon `;` after `num * num`""" # IF [[exercises]] name = "if1" -path = "exercises/03_if/if1.rs" -mode = "test" +dir = "03_if" hint = """ It's possible to do this in one line if you would like! @@ -175,23 +200,23 @@ Some similar examples from other languages: - In Python this would be: `a if a > b else b` Remember in Rust that: -- the `if` condition does not need to be surrounded by parentheses +- The `if` condition does not need to be surrounded by parentheses - `if`/`else` conditionals are expressions -- Each condition is followed by a `{}` block.""" +- Each condition is followed by a `{}` block""" [[exercises]] name = "if2" -path = "exercises/03_if/if2.rs" -mode = "test" +dir = "03_if" hint = """ For that first compiler error, it's important in Rust that each conditional -block returns the same type! To get the tests passing, you will need a couple -conditions checking different input values.""" +block returns the same type! + +To get the tests passing, you will need a couple conditions checking different +input values. Read the tests to find out what they expect.""" [[exercises]] name = "if3" -path = "exercises/03_if/if3.rs" -mode = "test" +dir = "03_if" hint = """ In Rust, every arm of an `if` expression has to return the same type of value. Make sure the type is consistent across all arms.""" @@ -200,30 +225,32 @@ Make sure the type is consistent across all arms.""" [[exercises]] name = "quiz1" -path = "exercises/quiz1.rs" -mode = "test" +dir = "quizzes" hint = "No hints this time ;)" # PRIMITIVE TYPES [[exercises]] name = "primitive_types1" -path = "exercises/04_primitive_types/primitive_types1.rs" -mode = "compile" -hint = "No hints this time ;)" +dir = "04_primitive_types" +test = false +hint = """ +In Rust, a boolean can be negated using the operator `!` before it. +Example: `!true == false` +This also works with boolean variables.""" [[exercises]] name = "primitive_types2" -path = "exercises/04_primitive_types/primitive_types2.rs" -mode = "compile" +dir = "04_primitive_types" +test = false hint = "No hints this time ;)" [[exercises]] name = "primitive_types3" -path = "exercises/04_primitive_types/primitive_types3.rs" -mode = "compile" +dir = "04_primitive_types" +test = false hint = """ -There's a shorthand to initialize Arrays with a certain size that does not +There's a shorthand to initialize arrays with a certain size that doesn't require you to type in 100 items (but you certainly can if you want!). For example, you can do: @@ -236,13 +263,12 @@ for `a.len() >= 100`?""" [[exercises]] name = "primitive_types4" -path = "exercises/04_primitive_types/primitive_types4.rs" -mode = "test" +dir = "04_primitive_types" hint = """ Take a look at the 'Understanding Ownership -> Slices -> Other Slices' section of the book: https://doc.rust-lang.org/book/ch04-03-slices.html and use the -starting and ending (plus one) indices of the items in the `Array` that you -want to end up in the slice. +starting and ending (plus one) indices of the items in the array that you want +to end up in the slice. If you're curious why the first argument of `assert_eq!` does not have an ampersand for a reference since the second argument is a reference, take a look @@ -251,8 +277,8 @@ https://doc.rust-lang.org/nomicon/coercions.html""" [[exercises]] name = "primitive_types5" -path = "exercises/04_primitive_types/primitive_types5.rs" -mode = "compile" +dir = "04_primitive_types" +test = false hint = """ Take a look at the 'Data Types -> The Tuple Type' section of the book: https://doc.rust-lang.org/book/ch03-02-data-types.html#the-tuple-type @@ -260,31 +286,30 @@ Particularly the part about destructuring (second to last example in the section). You'll need to make a pattern to bind `name` and `age` to the appropriate parts -of the tuple. You can do it!!""" +of the tuple.""" [[exercises]] name = "primitive_types6" -path = "exercises/04_primitive_types/primitive_types6.rs" -mode = "test" +dir = "04_primitive_types" hint = """ While you could use a destructuring `let` for the tuple here, try indexing into it instead, as explained in the last example of the 'Data Types -> The Tuple Type' section of the book: https://doc.rust-lang.org/book/ch03-02-data-types.html#the-tuple-type -Now you have another tool in your toolbox!""" +Now, you have another tool in your toolbox!""" # VECS [[exercises]] name = "vecs1" -path = "exercises/05_vecs/vecs1.rs" -mode = "test" +dir = "05_vecs" hint = """ In Rust, there are two ways to define a Vector. 1. One way is to use the `Vec::new()` function to create a new vector and fill it with the `push()` method. -2. The second way, which is simpler is to use the `vec![]` macro and - define your elements inside the square brackets. +2. The second way is to use the `vec![]` macro and define your elements + inside the square brackets. This way is simpler when you exactly know + the initial values. Check this chapter: https://doc.rust-lang.org/stable/book/ch08-01-vectors.html of the Rust book to learn more. @@ -292,18 +317,12 @@ of the Rust book to learn more. [[exercises]] name = "vecs2" -path = "exercises/05_vecs/vecs2.rs" -mode = "test" +dir = "05_vecs" hint = """ -In the first function we are looping over the Vector and getting a reference to -one `element` at a time. +In the first function, we create an empty vector and want to push new elements +to it. -To modify the value of that `element` we need to use the `*` dereference -operator. You can learn more in this chapter of the Rust book: -https://doc.rust-lang.org/stable/book/ch08-01-vectors.html#iterating-over-the-values-in-a-vector - -In the second function this dereferencing is not necessary, because the `map` -function expects the new value to be returned. +In the second function, we map the values of the input and collect them into a vector. After you've completed both functions, decide for yourself which approach you like better. @@ -315,8 +334,7 @@ What do you think is the more commonly used pattern under Rust developers? [[exercises]] name = "move_semantics1" -path = "exercises/06_move_semantics/move_semantics1.rs" -mode = "test" +dir = "06_move_semantics" hint = """ So you've got the "cannot borrow `vec` as mutable, as it is not declared as mutable" error on the line where we push an element to the vector, right? @@ -324,34 +342,25 @@ error on the line where we push an element to the vector, right? The fix for this is going to be adding one keyword, and the addition is NOT on the line where we push to the vector (where the error is). -Also: Try accessing `vec0` after having called `fill_vec()`. See what -happens!""" +Try accessing `vec0` after having called `fill_vec()`. See what happens!""" [[exercises]] name = "move_semantics2" -path = "exercises/06_move_semantics/move_semantics2.rs" -mode = "test" +dir = "06_move_semantics" hint = """ When running this exercise for the first time, you'll notice an error about "borrow of moved value". In Rust, when an argument is passed to a function and it's not explicitly returned, you can't use the original variable anymore. We call this "moving" a variable. When we pass `vec0` into `fill_vec`, it's -being "moved" into `vec1`, meaning we can't access `vec0` anymore after the -fact. +being "moved" into `vec1`, meaning we can't access `vec0` anymore. -Rust provides a couple of different ways to mitigate this issue, feel free to -try them all: -1. You could make another, separate version of the data that's in `vec0` and - pass that to `fill_vec` instead. -2. Make `fill_vec` borrow its argument instead of taking ownership of it, - and then copy the data within the function (`vec.clone()`) in order to - return an owned `Vec`. +You could make another, separate version of the data that's in `vec0` and +pass it to `fill_vec` instead. """ [[exercises]] name = "move_semantics3" -path = "exercises/06_move_semantics/move_semantics3.rs" -mode = "test" +dir = "06_move_semantics" hint = """ The difference between this one and the previous ones is that the first line of `fn fill_vec` that had `let mut vec = vec;` is no longer there. You can, @@ -360,87 +369,64 @@ an existing binding to be a mutable binding instead of an immutable one :)""" [[exercises]] name = "move_semantics4" -path = "exercises/06_move_semantics/move_semantics4.rs" -mode = "test" -hint = """ -Stop reading whenever you feel like you have enough direction :) Or try -doing one step and then fixing the compiler errors that result! -So the end goal is to: - - get rid of the first line in main that creates the new vector - - so then `vec0` doesn't exist, so we can't pass it to `fill_vec` - - `fill_vec` has had its signature changed, which our call should reflect - - since we're not creating a new vec in `main` anymore, we need to create - a new vec in `fill_vec`, and fill it with the expected values""" - -[[exercises]] -name = "move_semantics5" -path = "exercises/06_move_semantics/move_semantics5.rs" -mode = "test" +dir = "06_move_semantics" hint = """ Carefully reason about the range in which each mutable reference is in scope. Does it help to update the value of `x` immediately after -the mutable reference is taken? Read more about 'Mutable References' -in the book's section 'References and Borrowing': +the mutable reference is taken? +Read more about 'Mutable References' in the book's section 'References and Borrowing': https://doc.rust-lang.org/book/ch04-02-references-and-borrowing.html#mutable-references. """ [[exercises]] -name = "move_semantics6" -path = "exercises/06_move_semantics/move_semantics6.rs" -mode = "compile" +name = "move_semantics5" +dir = "06_move_semantics" +test = false hint = """ To find the answer, you can consult the book section "References and Borrowing": https://doc.rust-lang.org/stable/book/ch04-02-references-and-borrowing.html The first problem is that `get_char` is taking ownership of the string. So `data` is moved and can't be used for `string_uppercase`. `data` is moved to -`get_char` first, meaning that `string_uppercase` cannot manipulate the data. +`get_char` first, meaning that `string_uppercase` can't manipulate the data. Once you've fixed that, `string_uppercase`'s function signature will also need -to be adjusted. - -Can you figure out how? - -Another hint: it has to do with the `&` character.""" +to be adjusted.""" # STRUCTS [[exercises]] name = "structs1" -path = "exercises/07_structs/structs1.rs" -mode = "test" +dir = "07_structs" hint = """ Rust has more than one type of struct. Three actually, all variants are used to package related data together. -There are normal (or classic) structs. These are named collections of related -data stored in fields. +There are regular structs. These are named collections of related data stored in +fields. Tuple structs are basically just named tuples. -Finally, Unit-like structs. These don't have any fields and are useful for -generics. +Finally, unit structs. These don't have any fields and are useful for generics. -In this exercise you need to complete and implement one of each kind. +In this exercise, you need to complete and implement one of each kind. Read more about structs in The Book: https://doc.rust-lang.org/book/ch05-01-defining-structs.html""" [[exercises]] name = "structs2" -path = "exercises/07_structs/structs2.rs" -mode = "test" +dir = "07_structs" hint = """ Creating instances of structs is easy, all you need to do is assign some values to its fields. There are however some shortcuts that can be taken when instantiating structs. -Have a look in The Book, to find out more: +Have a look in The Book to find out more: https://doc.rust-lang.org/stable/book/ch05-01-defining-structs.html#creating-instances-from-other-instances-with-struct-update-syntax""" [[exercises]] name = "structs3" -path = "exercises/07_structs/structs3.rs" -mode = "test" +dir = "07_structs" hint = """ For `is_international`: What makes a package international? Seems related to the places it goes through right? @@ -448,43 +434,42 @@ the places it goes through right? For `get_fees`: This method takes an additional argument, is there a field in the `Package` struct that this relates to? -Have a look in The Book, to find out more about method implementations: +Have a look in The Book to find out more about method implementations: https://doc.rust-lang.org/book/ch05-03-method-syntax.html""" # ENUMS [[exercises]] name = "enums1" -path = "exercises/08_enums/enums1.rs" -mode = "compile" +dir = "08_enums" +test = false hint = "No hints this time ;)" [[exercises]] name = "enums2" -path = "exercises/08_enums/enums2.rs" -mode = "compile" +dir = "08_enums" +test = false hint = """ You can create enumerations that have different variants with different types -such as no data, anonymous structs, a single string, tuples, ...etc""" +such as no data, anonymous structs, a single string, tuples, etc.""" [[exercises]] name = "enums3" -path = "exercises/08_enums/enums3.rs" -mode = "test" +dir = "08_enums" hint = """ -As a first step, you can define enums to compile this code without errors. +As a first step, define enums to compile the code without errors. -And then create a match expression in `process()`. +Then, create a match expression in `process()`. Note that you need to deconstruct some message variants in the match expression -to get value in the variant.""" +to get the variant's values.""" # STRINGS [[exercises]] name = "strings1" -path = "exercises/09_strings/strings1.rs" -mode = "compile" +dir = "09_strings" +test = false hint = """ The `current_favorite_color` function is currently returning a string slice with the `'static` lifetime. We know this because the data of the string lives @@ -497,11 +482,11 @@ another way that uses the `From` trait.""" [[exercises]] name = "strings2" -path = "exercises/09_strings/strings2.rs" -mode = "compile" +dir = "09_strings" +test = false hint = """ Yes, it would be really easy to fix this by just changing the value bound to -`word` to be a string slice instead of a `String`, wouldn't it?? There is a way +`word` to be a string slice instead of a `String`, wouldn't it? There is a way to add one character to the `if` statement, though, that will coerce the `String` into a string slice. @@ -512,10 +497,10 @@ https://doc.rust-lang.org/stable/book/ch15-02-deref.html#implicit-deref-coercion [[exercises]] name = "strings3" -path = "exercises/09_strings/strings3.rs" -mode = "test" +dir = "09_strings" hint = """ -There's tons of useful standard library functions for strings. Let's try and use some of them: +There are many useful standard library functions for strings. Let's try and use +some of them: https://doc.rust-lang.org/std/string/struct.String.html#method.trim For the `compose_me` method: You can either use the `format!` macro, or convert @@ -523,137 +508,138 @@ the string slice into an owned string, which you can then freely extend.""" [[exercises]] name = "strings4" -path = "exercises/09_strings/strings4.rs" -mode = "compile" -hint = "No hints this time ;)" +dir = "09_strings" +test = false +hint = """ +Replace `placeholder` with either `string` or `string_slice` in the `main` function. + +Example: +`placeholder("blue");` +should become +`string_slice("blue");` +because "blue" is `&str`, not `String`. +""" # MODULES [[exercises]] name = "modules1" -path = "exercises/10_modules/modules1.rs" -mode = "compile" +dir = "10_modules" +test = false hint = """ -Everything is private in Rust by default-- but there's a keyword we can use -to make something public! The compiler error should point to the thing that -needs to be public.""" +Everything is private in Rust by default. But there's a keyword we can use +to make something public!""" [[exercises]] name = "modules2" -path = "exercises/10_modules/modules2.rs" -mode = "compile" +dir = "10_modules" +test = false hint = """ -The delicious_snacks module is trying to present an external interface that is -different than its internal structure (the `fruits` and `veggies` modules and -associated constants). Complete the `use` statements to fit the uses in main and -find the one keyword missing for both constants. +The `delicious_snacks` module is trying to present an external interface that +is different than its internal structure (the `fruits` and `veggies` modules +and associated constants). Complete the `use` statements to fit the uses in +`main` and find the one keyword missing for both constants. -Learn more at https://doc.rust-lang.org/book/ch07-04-bringing-paths-into-scope-with-the-use-keyword.html#re-exporting-names-with-pub-use""" +Learn more in The Book: +https://doc.rust-lang.org/book/ch07-04-bringing-paths-into-scope-with-the-use-keyword.html#re-exporting-names-with-pub-use""" [[exercises]] name = "modules3" -path = "exercises/10_modules/modules3.rs" -mode = "compile" +dir = "10_modules" +test = false hint = """ `UNIX_EPOCH` and `SystemTime` are declared in the `std::time` module. Add a `use` statement for these two to bring them into scope. You can use nested -paths or the glob operator to bring these two in using only one line.""" +paths to bring these two in using only one line.""" # HASHMAPS [[exercises]] name = "hashmaps1" -path = "exercises/11_hashmaps/hashmaps1.rs" -mode = "test" +dir = "11_hashmaps" hint = """ -Hint 1: Take a look at the return type of the function to figure out - the type for the `basket`. - -Hint 2: Number of fruits should be at least 5. And you have to put - at least three different types of fruits. -""" +The number of fruits should be at least 5 and you have to put at least 3 +different types of fruits.""" [[exercises]] name = "hashmaps2" -path = "exercises/11_hashmaps/hashmaps2.rs" -mode = "test" +dir = "11_hashmaps" hint = """ Use the `entry()` and `or_insert()` methods of `HashMap` to achieve this. -Learn more at https://doc.rust-lang.org/stable/book/ch08-03-hash-maps.html#only-inserting-a-value-if-the-key-has-no-value -""" + +Learn more in The Book: +https://doc.rust-lang.org/stable/book/ch08-03-hash-maps.html#only-inserting-a-value-if-the-key-has-no-value""" [[exercises]] name = "hashmaps3" -path = "exercises/11_hashmaps/hashmaps3.rs" -mode = "test" +dir = "11_hashmaps" hint = """ -Hint 1: Use the `entry()` and `or_insert()` methods of `HashMap` to insert - entries corresponding to each team in the scores table. +Hint 1: Use the `entry()` and `or_insert()` (or `or_insert_with()`) methods of + `HashMap` to insert the default value of `Team` if a team doesn't + exist in the table yet. -Learn more at https://doc.rust-lang.org/stable/book/ch08-03-hash-maps.html#only-inserting-a-value-if-the-key-has-no-value +Learn more in The Book: +https://doc.rust-lang.org/stable/book/ch08-03-hash-maps.html#only-inserting-a-value-if-the-key-has-no-value Hint 2: If there is already an entry for a given key, the value returned by `entry()` can be updated based on the existing value. -Learn more at https://doc.rust-lang.org/book/ch08-03-hash-maps.html#updating-a-value-based-on-the-old-value -""" +Learn more in The Book: +https://doc.rust-lang.org/book/ch08-03-hash-maps.html#updating-a-value-based-on-the-old-value""" # QUIZ 2 [[exercises]] name = "quiz2" -path = "exercises/quiz2.rs" -mode = "test" +dir = "quizzes" hint = "No hints this time ;)" # OPTIONS [[exercises]] name = "options1" -path = "exercises/12_options/options1.rs" -mode = "test" +dir = "12_options" hint = """ Options can have a `Some` value, with an inner value, or a `None` value, without an inner value. -There's multiple ways to get at the inner value, you can use `unwrap`, or +There are multiple ways to get at the inner value, you can use `unwrap`, or pattern match. Unwrapping is the easiest, but how do you do it safely so that it doesn't panic in your face later?""" [[exercises]] name = "options2" -path = "exercises/12_options/options2.rs" -mode = "test" +dir = "12_options" hint = """ Check out: - https://doc.rust-lang.org/rust-by-example/flow_control/if_let.html - https://doc.rust-lang.org/rust-by-example/flow_control/while_let.html -Remember that `Option`s can be stacked in `if let` and `while let`. +Remember that `Option`s can be nested in if-let and while-let statements. -For example: `Some(Some(variable)) = variable2` +For example: `if let Some(Some(x)) = y` Also see `Option::flatten` """ [[exercises]] name = "options3" -path = "exercises/12_options/options3.rs" -mode = "compile" +dir = "12_options" +test = false hint = """ The compiler says a partial move happened in the `match` statement. How can this be avoided? The compiler shows the correction needed. -After making the correction as suggested by the compiler, do read: +After making the correction as suggested by the compiler, read the related docs +page: https://doc.rust-lang.org/std/keyword.ref.html""" # ERROR HANDLING [[exercises]] name = "errors1" -path = "exercises/13_error_handling/errors1.rs" -mode = "test" +dir = "13_error_handling" hint = """ `Ok` and `Err` are the two variants of `Result`, so what the tests are saying is that `generate_nametag_text` should return a `Result` instead of an `Option`. @@ -661,91 +647,80 @@ is that `generate_nametag_text` should return a `Result` instead of an `Option`. To make this change, you'll need to: - update the return type in the function signature to be a `Result` that could be the variants `Ok(String)` and `Err(String)` - - change the body of the function to return `Ok(stuff)` where it currently - returns `Some(stuff)` + - change the body of the function to return `Ok(…)` where it currently + returns `Some(…)` - change the body of the function to return `Err(error message)` where it currently returns `None`""" [[exercises]] name = "errors2" -path = "exercises/13_error_handling/errors2.rs" -mode = "test" +dir = "13_error_handling" hint = """ One way to handle this is using a `match` statement on `item_quantity.parse::()` where the cases are `Ok(something)` and `Err(something)`. -This pattern is very common in Rust, though, so there's a `?` operator that +This pattern is very common in Rust, though, so there's the `?` operator that does pretty much what you would make that match statement do for you! -Take a look at this section of the 'Error Handling' chapter: -https://doc.rust-lang.org/book/ch09-02-recoverable-errors-with-result.html#a-shortcut-for-propagating-errors-the--operator -and give it a try!""" +Take a look at this section of the "Error Handling" chapter: +https://doc.rust-lang.org/book/ch09-02-recoverable-errors-with-result.html#a-shortcut-for-propagating-errors-the--operator""" [[exercises]] name = "errors3" -path = "exercises/13_error_handling/errors3.rs" -mode = "compile" +dir = "13_error_handling" +test = false hint = """ If other functions can return a `Result`, why shouldn't `main`? It's a fairly common convention to return something like `Result<(), ErrorType>` from your -main function. +`main` function. -The unit (`()`) type is there because nothing is really needed in terms of -positive results.""" +The unit type `()` is there because nothing is really needed in terms of a +positive result.""" [[exercises]] name = "errors4" -path = "exercises/13_error_handling/errors4.rs" -mode = "test" +dir = "13_error_handling" hint = """ `PositiveNonzeroInteger::new` is always creating a new instance and returning -an `Ok` result. - -It should be doing some checking, returning an `Err` result if those checks -fail, and only returning an `Ok` result if those checks determine that -everything is... okay :)""" +an `Ok` result. But it should be doing some checking, returning an `Err` if +those checks fail, and only returning an `Ok` if those checks determine that +everything is… okay :)""" [[exercises]] name = "errors5" -path = "exercises/13_error_handling/errors5.rs" -mode = "compile" +dir = "13_error_handling" +test = false hint = """ -There are two different possible `Result` types produced within `main()`, which -are propagated using `?` operators. How do we declare a return type from -`main()` that allows both? +There are two different possible `Result` types produced within the `main` +function, which are propagated using the `?` operators. How do we declare a +return type for the `main` function that allows both? Under the hood, the `?` operator calls `From::from` on the error value to -convert it to a boxed trait object, a `Box`. This boxed trait -object is polymorphic, and since all errors implement the `error::Error` trait, -we can capture lots of different errors in one "Box" object. +convert it to a boxed trait object, a `Box`. This boxed trait object +is polymorphic, and since all errors implement the `Error` trait, we can capture +lots of different errors in one `Box` object. -Check out this section of the book: +Check out this section of The Book: https://doc.rust-lang.org/book/ch09-02-recoverable-errors-with-result.html#a-shortcut-for-propagating-errors-the--operator Read more about boxing errors: https://doc.rust-lang.org/stable/rust-by-example/error/multiple_error_types/boxing_errors.html Read more about using the `?` operator with boxed errors: -https://doc.rust-lang.org/stable/rust-by-example/error/multiple_error_types/reenter_question_mark.html -""" +https://doc.rust-lang.org/stable/rust-by-example/error/multiple_error_types/reenter_question_mark.html""" [[exercises]] name = "errors6" -path = "exercises/13_error_handling/errors6.rs" -mode = "test" +dir = "13_error_handling" hint = """ -This exercise uses a completed version of `PositiveNonzeroInteger` from -errors4. +This exercise uses a completed version of `PositiveNonzeroInteger` from the +previous exercises. Below the line that `TODO` asks you to change, there is an example of using the `map_err()` method on a `Result` to transform one type of error into another. Try using something similar on the `Result` from `parse()`. You -might use the `?` operator to return early from the function, or you might -use a `match` expression, or maybe there's another way! - -You can create another function inside `impl ParsePosNonzeroError` to use -with `map_err()`. +can then use the `?` operator to return early. Read more about `map_err()` in the `std::result` documentation: https://doc.rust-lang.org/std/result/enum.Result.html#method.map_err""" @@ -754,110 +729,106 @@ https://doc.rust-lang.org/std/result/enum.Result.html#method.map_err""" [[exercises]] name = "generics1" -path = "exercises/14_generics/generics1.rs" -mode = "compile" +dir = "14_generics" +test = false hint = """ Vectors in Rust make use of generics to create dynamically sized arrays of any type. +If the vector `numbers` has the type `Vec`, then we can only push values of +type `T` to it. By using `into()` before pushing, we ask the compiler to convert +`n1` and `n2` to `T`. But the compiler doesn't know what `T` is yet and needs a +type annotation. -You need to tell the compiler what type we are pushing onto this vector.""" +`u8` and `i8` can both be converted to `i16`, `i32` and `i64`. Choose one for +the generic of the vector.""" [[exercises]] name = "generics2" -path = "exercises/14_generics/generics2.rs" -mode = "test" +dir = "14_generics" hint = """ -Currently we are wrapping only values of type `u32`. - -Maybe we could update the explicit references to this data type somehow? - -If you are still stuck https://doc.rust-lang.org/stable/book/ch10-01-syntax.html#in-method-definitions -""" +Related section in The Book: +https://doc.rust-lang.org/stable/book/ch10-01-syntax.html#in-method-definitions""" # TRAITS [[exercises]] name = "traits1" -path = "exercises/15_traits/traits1.rs" -mode = "test" +dir = "15_traits" hint = """ -A discussion about Traits in Rust can be found at: -https://doc.rust-lang.org/book/ch10-02-traits.html -""" +More about traits in The Book: +https://doc.rust-lang.org/book/ch10-02-traits.html""" [[exercises]] name = "traits2" -path = "exercises/15_traits/traits2.rs" -mode = "test" +dir = "15_traits" hint = """ -Notice how the trait takes ownership of `self`, and returns `Self`. +Notice how the trait takes ownership of `self` and returns `Self`. -Try mutating the incoming string vector. Have a look at the tests to see -what the result should look like! - -Vectors provide suitable methods for adding an element at the end. See -the documentation at: https://doc.rust-lang.org/std/vec/struct.Vec.html""" +Although the signature of `append_bar` in the trait takes `self` as argument, +the implementation can take `mut self` instead. This is possible because the +the value is owned anyway.""" [[exercises]] name = "traits3" -path = "exercises/15_traits/traits3.rs" -mode = "test" +dir = "15_traits" hint = """ -Traits can have a default implementation for functions. Structs that implement -the trait can then use the default version of these functions if they choose not -to implement the function themselves. +Traits can have a default implementation for functions. Data types that +implement the trait can then use the default version of these functions +if they choose not to implement the function themselves. -See the documentation at: https://doc.rust-lang.org/book/ch10-02-traits.html#default-implementations -""" +Related section in The Book: +https://doc.rust-lang.org/book/ch10-02-traits.html#default-implementations""" [[exercises]] name = "traits4" -path = "exercises/15_traits/traits4.rs" -mode = "test" +dir = "15_traits" hint = """ Instead of using concrete types as parameters you can use traits. Try replacing -the '??' with 'impl [what goes here?]' +`???` with `impl [what goes here?]`. -See the documentation at: https://doc.rust-lang.org/book/ch10-02-traits.html#traits-as-parameters -""" +Related section in The Book: +https://doc.rust-lang.org/book/ch10-02-traits.html#traits-as-parameters""" [[exercises]] name = "traits5" -path = "exercises/15_traits/traits5.rs" -mode = "compile" +dir = "15_traits" hint = """ To ensure a parameter implements multiple traits use the '+ syntax'. Try -replacing the '??' with 'impl [what goes here?] + [what goes here?]'. +replacing `???` with 'impl [what goes here?] + [what goes here?]'. -See the documentation at: https://doc.rust-lang.org/book/ch10-02-traits.html#specifying-multiple-trait-bounds-with-the--syntax -""" +Related section in The Book: +https://doc.rust-lang.org/book/ch10-02-traits.html#specifying-multiple-trait-bounds-with-the--syntax""" # QUIZ 3 [[exercises]] name = "quiz3" -path = "exercises/quiz3.rs" -mode = "test" +dir = "quizzes" hint = """ -To find the best solution to this challenge you're going to need to think back -to your knowledge of traits, specifically 'Trait Bound Syntax' +To find the best solution to this challenge, you need to recall your knowledge +of traits, specifically "Trait Bound Syntax": +https://doc.rust-lang.org/book/ch10-02-traits.html#trait-bound-syntax -You may also need this: `use std::fmt::Display;`.""" +Here is how to specify a trait bound for an implementation block: +`impl for Foo { … }` + +You may need this: +`use std::fmt::Display;` +""" # LIFETIMES [[exercises]] name = "lifetimes1" -path = "exercises/16_lifetimes/lifetimes1.rs" -mode = "compile" +dir = "16_lifetimes" hint = """ -Let the compiler guide you. Also take a look at the book if you need help: +Let the compiler guide you. Also take a look at The Book if you need help: https://doc.rust-lang.org/book/ch10-03-lifetime-syntax.html""" [[exercises]] name = "lifetimes2" -path = "exercises/16_lifetimes/lifetimes2.rs" -mode = "compile" +dir = "16_lifetimes" +test = false hint = """ Remember that the generic lifetime `'a` will get the concrete lifetime that is equal to the smaller of the lifetimes of `x` and `y`. @@ -870,61 +841,41 @@ inner block: [[exercises]] name = "lifetimes3" -path = "exercises/16_lifetimes/lifetimes3.rs" -mode = "compile" -hint = """ -If you use a lifetime annotation in a struct's fields, where else does it need -to be added?""" +dir = "16_lifetimes" +test = false +hint = """Let the compiler guide you :)""" # TESTS [[exercises]] name = "tests1" -path = "exercises/17_tests/tests1.rs" -mode = "test" +dir = "17_tests" hint = """ -You don't even need to write any code to test -- you can just test values and -run that, even though you wouldn't do that in real life. :) - `assert!` is a macro that needs an argument. Depending on the value of the argument, `assert!` will do nothing (in which case the test will pass) or `assert!` will panic (in which case the test will fail). So try giving different values to `assert!` and see which ones compile, which -ones pass, and which ones fail :)""" +ones pass, and which ones fail :) + +If you want to check for `false`, you can negate the result of what you're +checking using `!`, like `assert!(!…)`.""" [[exercises]] name = "tests2" -path = "exercises/17_tests/tests2.rs" -mode = "test" +dir = "17_tests" hint = """ -Like the previous exercise, you don't need to write any code to get this test -to compile and run. - `assert_eq!` is a macro that takes two arguments and compares them. Try giving it two values that are equal! Try giving it two arguments that are different! -Try giving it two values that are of different types! Try switching which -argument comes first and which comes second!""" +Try switching which argument comes first and which comes second!""" [[exercises]] name = "tests3" -path = "exercises/17_tests/tests3.rs" -mode = "test" +dir = "17_tests" hint = """ -You can call a function right where you're passing arguments to `assert!`. So -you could do something like `assert!(having_fun())`. +We expect the method `Rectangle::new` to panic for negative values. -If you want to check that you indeed get `false`, you can negate the result of -what you're doing using `!`, like `assert!(!having_fun())`.""" - -[[exercises]] -name = "tests4" -path = "exercises/17_tests/tests4.rs" -mode = "test" -hint = """ -We expect method `Rectangle::new()` to panic for negative values. - -To handle that you need to add a special attribute to the test function. +To handle that, you need to add a special attribute to the test function. You can refer to the docs: https://doc.rust-lang.org/stable/book/ch11-01-writing-tests.html#checking-for-panics-with-should_panic""" @@ -933,33 +884,17 @@ https://doc.rust-lang.org/stable/book/ch11-01-writing-tests.html#checking-for-pa [[exercises]] name = "iterators1" -path = "exercises/18_iterators/iterators1.rs" -mode = "test" +dir = "18_iterators" hint = """ -Step 1: - -We need to apply something to the collection `my_fav_fruits` before we start to -go through it. What could that be? Take a look at the struct definition for a -vector for inspiration: -https://doc.rust-lang.org/std/vec/struct.Vec.html - -Step 2 & step 3: - -Very similar to the lines above and below. You've got this! - -Step 4: - An iterator goes through all elements in a collection, but what if we've run out of elements? What should we expect here? If you're stuck, take a look at -https://doc.rust-lang.org/std/iter/trait.Iterator.html for some ideas. -""" +https://doc.rust-lang.org/std/iter/trait.Iterator.html""" [[exercises]] name = "iterators2" -path = "exercises/18_iterators/iterators2.rs" -mode = "test" +dir = "18_iterators" hint = """ -Step 1: +`capitalize_first`: The variable `first` is a `char`. It needs to be capitalized and added to the remaining characters in `c` in order to return the correct `String`. @@ -970,23 +905,25 @@ The remaining characters in `c` can be viewed as a string slice using the The documentation for `char` contains many useful methods. https://doc.rust-lang.org/std/primitive.char.html -Step 2: +Use `char::to_uppercase`. It returns an iterator that can be converted to a +`String`. + +`capitalize_words_vector`: Create an iterator from the slice. Transform the iterated values by applying the `capitalize_first` function. Remember to `collect` the iterator. -Step 3: +`capitalize_words_string`: This is surprisingly similar to the previous solution. `collect` is very powerful and very general. Rust just needs to know the desired type.""" [[exercises]] name = "iterators3" -path = "exercises/18_iterators/iterators3.rs" -mode = "test" +dir = "18_iterators" hint = """ -The `divide` function needs to return the correct error when even division is -not possible. +The `divide` function needs to return the correct error when the divisor is 0 or +when even division is not possible. The `division_results` variable needs to be collected into a collection type. @@ -997,24 +934,22 @@ The `list_of_results` function needs to return a vector of results. See https://doc.rust-lang.org/std/iter/trait.Iterator.html#method.collect for how the `FromIterator` trait is used in `collect()`. This trait is REALLY -powerful! It can make the solution to this exercise infinitely easier.""" +powerful! It can make the solution to this exercise much easier.""" [[exercises]] name = "iterators4" -path = "exercises/18_iterators/iterators4.rs" -mode = "test" +dir = "18_iterators" hint = """ In an imperative language, you might write a `for` loop that updates a mutable variable. Or, you might write code utilizing recursion and a match clause. In -Rust you can take another functional approach, computing the factorial +Rust, you can take another functional approach, computing the factorial elegantly with ranges and iterators. -Hint 2: Check out the `fold` and `rfold` methods!""" +Check out the `fold` and `rfold` methods!""" [[exercises]] name = "iterators5" -path = "exercises/18_iterators/iterators5.rs" -mode = "test" +dir = "18_iterators" hint = """ The documentation for the `std::iter::Iterator` trait contains numerous methods that would be helpful here. @@ -1032,32 +967,24 @@ a different method that could make your code more compact than using `fold`.""" [[exercises]] name = "box1" -path = "exercises/19_smart_pointers/box1.rs" -mode = "test" +dir = "19_smart_pointers" hint = """ -Step 1: - -The compiler's message should help: since we cannot store the value of the +The compiler's message should help: Since we cannot store the value of the actual type when working with recursive types, we need to store a reference (pointer) to its value. -We should, therefore, place our `List` inside a `Box`. More details in the book -here: https://doc.rust-lang.org/book/ch15-01-box.html#enabling-recursive-types-with-boxes +We should, therefore, place our `List` inside a `Box`. More details in The Book: +https://doc.rust-lang.org/book/ch15-01-box.html#enabling-recursive-types-with-boxes -Step 2: +Creating an empty list should be fairly straightforward (Hint: Read the tests). -Creating an empty list should be fairly straightforward (hint: peek at the -assertions). - -For a non-empty list keep in mind that we want to use our `Cons` "list builder". +For a non-empty list, keep in mind that we want to use our `Cons` list builder. Although the current list is one of integers (`i32`), feel free to change the -definition and try other types! -""" +definition and try other types!""" [[exercises]] name = "rc1" -path = "exercises/19_smart_pointers/rc1.rs" -mode = "test" +dir = "19_smart_pointers" hint = """ This is a straightforward exercise to use the `Rc` type. Each `Planet` has ownership of the `Sun`, and uses `Rc::clone()` to increment the reference count @@ -1066,19 +993,18 @@ of the `Sun`. After using `drop()` to move the `Planet`s out of scope individually, the reference count goes down. -In the end the `Sun` only has one reference again, to itself. +In the end, the `Sun` only has one reference again, to itself. See more at: https://doc.rust-lang.org/book/ch15-04-rc.html -* Unfortunately Pluto is no longer considered a planet :( -""" +Unfortunately, Pluto is no longer considered a planet :(""" [[exercises]] name = "arc1" -path = "exercises/19_smart_pointers/arc1.rs" -mode = "compile" +dir = "19_smart_pointers" +test = false hint = """ -Make `shared_numbers` be an `Arc` from the numbers vector. Then, in order +Make `shared_numbers` be an `Arc` from the `numbers` vector. Then, in order to avoid creating a copy of `numbers`, you'll need to create `child_numbers` inside the loop but still in the main thread. @@ -1086,69 +1012,63 @@ inside the loop but still in the main thread. thread-local copy of the numbers. This is a simple exercise if you understand the underlying concepts, but if this -is too much of a struggle, consider reading through all of Chapter 16 in the -book: -https://doc.rust-lang.org/stable/book/ch16-00-concurrency.html -""" +is too much of a struggle, consider reading through all of Chapter 16 in The +Book: +https://doc.rust-lang.org/stable/book/ch16-00-concurrency.html""" [[exercises]] name = "cow1" -path = "exercises/19_smart_pointers/cow1.rs" -mode = "test" +dir = "19_smart_pointers" hint = """ -If `Cow` already owns the data it doesn't need to clone it when `to_mut()` is +If `Cow` already owns the data, it doesn't need to clone it when `to_mut()` is called. -Check out https://doc.rust-lang.org/std/borrow/enum.Cow.html for documentation -on the `Cow` type. -""" +Check out the documentation of the `Cow` type: +https://doc.rust-lang.org/std/borrow/enum.Cow.html""" # THREADS [[exercises]] name = "threads1" -path = "exercises/20_threads/threads1.rs" -mode = "compile" +dir = "20_threads" +test = false hint = """ `JoinHandle` is a struct that is returned from a spawned thread: https://doc.rust-lang.org/std/thread/fn.spawn.html A challenge with multi-threaded applications is that the main thread can -finish before the spawned threads are completed. +finish before the spawned threads are done. https://doc.rust-lang.org/book/ch16-01-threads.html#waiting-for-all-threads-to-finish-using-join-handles Use the `JoinHandle`s to wait for each thread to finish and collect their results. -https://doc.rust-lang.org/std/thread/struct.JoinHandle.html -""" +https://doc.rust-lang.org/std/thread/struct.JoinHandle.html""" [[exercises]] name = "threads2" -path = "exercises/20_threads/threads2.rs" -mode = "compile" +dir = "20_threads" +test = false hint = """ `Arc` is an Atomic Reference Counted pointer that allows safe, shared access -to **immutable** data. But we want to *change* the number of `jobs_completed` -so we'll need to also use another type that will only allow one thread to -mutate the data at a time. Take a look at this section of the book: +to **immutable** data. But we want to *change* the number of `jobs_done` so +we'll need to also use another type that will only allow one thread to mutate +the data at a time. Take a look at this section of the book: https://doc.rust-lang.org/book/ch16-03-shared-state.html#atomic-reference-counting-with-arct Keep reading if you'd like more hints :) Do you now have an `Arc>` at the beginning of `main`? Like: ``` -let status = Arc::new(Mutex::new(JobStatus { jobs_completed: 0 })); +let status = Arc::new(Mutex::new(JobStatus { jobs_done: 0 })); ``` -Similar to the code in the following example in the book: -https://doc.rust-lang.org/book/ch16-03-shared-state.html#sharing-a-mutext-between-multiple-threads -""" +Similar to the code in the following example in The Book: +https://doc.rust-lang.org/book/ch16-03-shared-state.html#sharing-a-mutext-between-multiple-threads""" [[exercises]] name = "threads3" -path = "exercises/20_threads/threads3.rs" -mode = "test" +dir = "20_threads" hint = """ An alternate way to handle concurrency between threads is to use an `mpsc` (multiple producer, single consumer) channel to communicate. @@ -1156,27 +1076,26 @@ An alternate way to handle concurrency between threads is to use an `mpsc` With both a sending end and a receiving end, it's possible to send values in one thread and receive them in another. -Multiple producers are possible by using clone() to create a duplicate of the +Multiple producers are possible by using `clone()` to create a duplicate of the original sending end. -See https://doc.rust-lang.org/book/ch16-02-message-passing.html for more info. -""" +Related section in The Book: +https://doc.rust-lang.org/book/ch16-02-message-passing.html""" # MACROS [[exercises]] name = "macros1" -path = "exercises/21_macros/macros1.rs" -mode = "compile" +dir = "21_macros" +test = false hint = """ -When you call a macro, you need to add something special compared to a -regular function call. If you're stuck, take a look at what's inside -`my_macro`.""" +When you call a macro, you need to add something special compared to a regular +function call.""" [[exercises]] name = "macros2" -path = "exercises/21_macros/macros2.rs" -mode = "compile" +dir = "21_macros" +test = false hint = """ Macros don't quite play by the same rules as the rest of Rust, in terms of what's available where. @@ -1186,19 +1105,16 @@ Unlike other things in Rust, the order of "where you define a macro" versus [[exercises]] name = "macros3" -path = "exercises/21_macros/macros3.rs" -mode = "compile" +dir = "21_macros" +test = false hint = """ In order to use a macro outside of its module, you need to do something -special to the module to lift the macro out into its parent. - -The same trick also works on "extern crate" statements for crates that have -exported macros, if you've seen any of those around.""" +special to the module to lift the macro out into its parent.""" [[exercises]] name = "macros4" -path = "exercises/21_macros/macros4.rs" -mode = "compile" +dir = "21_macros" +test = false hint = """ You only need to add a single character to make this compile. @@ -1214,10 +1130,11 @@ https://veykril.github.io/tlborm/""" [[exercises]] name = "clippy1" -path = "exercises/22_clippy/clippy1.rs" -mode = "clippy" +dir = "22_clippy" +test = false +strict_clippy = true hint = """ -Rust stores the highest precision version of any long or infinite precision +Rust stores the highest precision version of some long or infinite precision mathematical constants in the Rust standard library: https://doc.rust-lang.org/stable/std/f32/consts/index.html @@ -1225,86 +1142,68 @@ We may be tempted to use our own approximations for certain mathematical constants, but clippy recognizes those imprecise mathematical constants as a source of potential error. -See the suggestions of the clippy warning in compile output and use the +See the suggestions of the Clippy warning in the compile output and use the appropriate replacement constant from `std::f32::consts`...""" [[exercises]] name = "clippy2" -path = "exercises/22_clippy/clippy2.rs" -mode = "clippy" +dir = "22_clippy" +test = false +strict_clippy = true hint = """ -`for` loops over `Option` values are more clearly expressed as an `if let`""" +`for` loops over `Option` values are more clearly expressed as an `if-let` +statement.""" [[exercises]] name = "clippy3" -path = "exercises/22_clippy/clippy3.rs" -mode = "clippy" +dir = "22_clippy" +test = false +strict_clippy = true hint = "No hints this time!" # TYPE CONVERSIONS [[exercises]] name = "using_as" -path = "exercises/23_conversions/using_as.rs" -mode = "test" +dir = "23_conversions" hint = """ Use the `as` operator to cast one of the operands in the last line of the `average` function into the expected return type.""" [[exercises]] name = "from_into" -path = "exercises/23_conversions/from_into.rs" -mode = "test" +dir = "23_conversions" hint = """ -Follow the steps provided right before the `From` implementation""" +Follow the steps provided right before the `From` implementation.""" [[exercises]] name = "from_str" -path = "exercises/23_conversions/from_str.rs" -mode = "test" +dir = "23_conversions" hint = """ The implementation of `FromStr` should return an `Ok` with a `Person` object, or an `Err` with an error if the string is not valid. -This is almost like the `from_into` exercise, but returning errors instead -of falling back to a default value. +This is almost like the previous `from_into` exercise, but returning errors +instead of falling back to a default value. -Look at the test cases to see which error variants to return. - -Another hint: You can use the `map_err` method of `Result` with a function -or a closure to wrap the error from `parse::`. +Another hint: You can use the `map_err` method of `Result` with a function or a +closure to wrap the error from `parse::`. Yet another hint: If you would like to propagate errors by using the `?` operator in your solution, you might want to look at -https://doc.rust-lang.org/stable/rust-by-example/error/multiple_error_types/reenter_question_mark.html -""" +https://doc.rust-lang.org/stable/rust-by-example/error/multiple_error_types/reenter_question_mark.html""" [[exercises]] name = "try_from_into" -path = "exercises/23_conversions/try_from_into.rs" -mode = "test" +dir = "23_conversions" hint = """ -Follow the steps provided right before the `TryFrom` implementation. -You can also use the example at -https://doc.rust-lang.org/std/convert/trait.TryFrom.html - -Is there an implementation of `TryFrom` in the standard library that -can both do the required integer conversion and check the range of the input? - -Another hint: Look at the test cases to see which error variants to return. - -Yet another hint: You can use the `map_err` or `or` methods of `Result` to -convert errors. - -Yet another hint: If you would like to propagate errors by using the `?` -operator in your solution, you might want to look at -https://doc.rust-lang.org/stable/rust-by-example/error/multiple_error_types/reenter_question_mark.html +Is there an implementation of `TryFrom` in the standard library that can both do +the required integer conversion and check the range of the input? Challenge: Can you make the `TryFrom` implementations generic over many integer types?""" [[exercises]] name = "as_ref_mut" -path = "exercises/23_conversions/as_ref_mut.rs" -mode = "test" +dir = "23_conversions" hint = """ Add `AsRef` or `AsMut` as a trait bound to the functions.""" diff --git a/rustlings-macros/src/lib.rs b/rustlings-macros/src/lib.rs new file mode 100644 index 00000000..6c6067bc --- /dev/null +++ b/rustlings-macros/src/lib.rs @@ -0,0 +1,56 @@ +use proc_macro::TokenStream; +use quote::quote; +use serde::Deserialize; + +#[derive(Deserialize)] +struct ExerciseInfo { + name: String, + dir: String, +} + +#[derive(Deserialize)] +struct InfoFile { + exercises: Vec, +} + +#[proc_macro] +pub fn include_files(_: TokenStream) -> TokenStream { + let info_file = include_str!("../info.toml"); + let exercises = toml_edit::de::from_str::(info_file) + .expect("Failed to parse `info.toml`") + .exercises; + + let exercise_files = exercises + .iter() + .map(|exercise| format!("../exercises/{}/{}.rs", exercise.dir, exercise.name)); + let solution_files = exercises + .iter() + .map(|exercise| format!("../solutions/{}/{}.rs", exercise.dir, exercise.name)); + + let mut dirs = Vec::with_capacity(32); + let mut dir_inds = vec![0; exercises.len()]; + + for (exercise, dir_ind) in exercises.iter().zip(&mut dir_inds) { + // The directory is often the last one inserted. + if let Some(ind) = dirs.iter().rev().position(|dir| *dir == exercise.dir) { + *dir_ind = dirs.len() - 1 - ind; + continue; + } + + dirs.push(exercise.dir.as_str()); + *dir_ind = dirs.len() - 1; + } + + let readmes = dirs + .iter() + .map(|dir| format!("../exercises/{dir}/README.md")); + + quote! { + EmbeddedFiles { + info_file: #info_file, + exercise_files: &[#(ExerciseFiles { exercise: include_bytes!(#exercise_files), solution: include_bytes!(#solution_files), dir_ind: #dir_inds }),*], + exercise_dirs: &[#(ExerciseDir { name: #dirs, readme: include_bytes!(#readmes) }),*] + } + } + .into() +} diff --git a/shell.nix b/shell.nix deleted file mode 100644 index fa2a56c7..00000000 --- a/shell.nix +++ /dev/null @@ -1,6 +0,0 @@ -(import (let lock = builtins.fromJSON (builtins.readFile ./flake.lock); -in fetchTarball { - url = - "https://github.com/edolstra/flake-compat/archive/${lock.nodes.flake-compat.locked.rev}.tar.gz"; - sha256 = lock.nodes.flake-compat.locked.narHash; -}) { src = ./.; }).shellNix diff --git a/solutions/00_intro/intro1.rs b/solutions/00_intro/intro1.rs new file mode 100644 index 00000000..4fe84549 --- /dev/null +++ b/solutions/00_intro/intro1.rs @@ -0,0 +1,5 @@ +fn main() { + // Congratulations, you finished the first exercise 🎉 + // As an introduction to Rustlings, the first exercise only required + // entering `n` in the terminal to go to the next exercise. +} diff --git a/solutions/00_intro/intro2.rs b/solutions/00_intro/intro2.rs new file mode 100644 index 00000000..b8e031a0 --- /dev/null +++ b/solutions/00_intro/intro2.rs @@ -0,0 +1,4 @@ +fn main() { + // `println!` instead of `printline!`. + println!("Hello world!"); +} diff --git a/solutions/01_variables/variables1.rs b/solutions/01_variables/variables1.rs new file mode 100644 index 00000000..58d046b2 --- /dev/null +++ b/solutions/01_variables/variables1.rs @@ -0,0 +1,6 @@ +fn main() { + // Declaring variables requires the `let` keyword. + let x = 5; + + println!("x has the value {x}"); +} diff --git a/solutions/01_variables/variables2.rs b/solutions/01_variables/variables2.rs new file mode 100644 index 00000000..50b8d1b4 --- /dev/null +++ b/solutions/01_variables/variables2.rs @@ -0,0 +1,16 @@ +fn main() { + // The easiest way to fix the compiler error is to initialize the + // variable `x`. By setting its value to an integer, Rust infers its type + // as `i32` which is the default type for integers. + let x = 42; + + // But we can enforce a type different from the default `i32` by adding + // a type annotation: + // let x: u8 = 42; + + if x == 10 { + println!("x is ten!"); + } else { + println!("x is not ten!"); + } +} diff --git a/solutions/01_variables/variables3.rs b/solutions/01_variables/variables3.rs new file mode 100644 index 00000000..7db42a95 --- /dev/null +++ b/solutions/01_variables/variables3.rs @@ -0,0 +1,13 @@ +fn main() { + // Reading uninitialized variables isn't allowed in Rust! + // Therefore, we need to assign a value first. + let x: i32 = 42; + + println!("Number {x}"); + + // It possible to declare a variable and initialize it later. + // But it can't be used before initialization. + let y: i32; + y = 42; + println!("Number {y}"); +} diff --git a/solutions/01_variables/variables4.rs b/solutions/01_variables/variables4.rs new file mode 100644 index 00000000..7de6bcbf --- /dev/null +++ b/solutions/01_variables/variables4.rs @@ -0,0 +1,9 @@ +fn main() { + // In Rust, variables are immutable by default. + // Adding the `mut` keyword after `let` makes the declared variable mutable. + let mut x = 3; + println!("Number {x}"); + + x = 5; + println!("Number {x}"); +} diff --git a/solutions/01_variables/variables5.rs b/solutions/01_variables/variables5.rs new file mode 100644 index 00000000..9057754c --- /dev/null +++ b/solutions/01_variables/variables5.rs @@ -0,0 +1,9 @@ +fn main() { + let number = "T-H-R-E-E"; + println!("Spell a number: {}", number); + + // Using variable shadowing + // https://doc.rust-lang.org/book/ch03-01-variables-and-mutability.html#shadowing + let number = 3; + println!("Number plus two is: {}", number + 2); +} diff --git a/solutions/01_variables/variables6.rs b/solutions/01_variables/variables6.rs new file mode 100644 index 00000000..25b7a1e4 --- /dev/null +++ b/solutions/01_variables/variables6.rs @@ -0,0 +1,6 @@ +// The type of constants must always be annotated. +const NUMBER: u64 = 3; + +fn main() { + println!("Number: {NUMBER}"); +} diff --git a/solutions/02_functions/functions1.rs b/solutions/02_functions/functions1.rs new file mode 100644 index 00000000..dc527446 --- /dev/null +++ b/solutions/02_functions/functions1.rs @@ -0,0 +1,8 @@ +// Some function with the name `call_me` without arguments or a return value. +fn call_me() { + println!("Hello world!"); +} + +fn main() { + call_me(); +} diff --git a/solutions/02_functions/functions2.rs b/solutions/02_functions/functions2.rs new file mode 100644 index 00000000..f14ffa35 --- /dev/null +++ b/solutions/02_functions/functions2.rs @@ -0,0 +1,11 @@ +// The type of function arguments must be annotated. +// Added the type annotation `u64`. +fn call_me(num: u64) { + for i in 0..num { + println!("Ring! Call number {}", i + 1); + } +} + +fn main() { + call_me(3); +} diff --git a/solutions/02_functions/functions3.rs b/solutions/02_functions/functions3.rs new file mode 100644 index 00000000..c581c425 --- /dev/null +++ b/solutions/02_functions/functions3.rs @@ -0,0 +1,10 @@ +fn call_me(num: u32) { + for i in 0..num { + println!("Ring! Call number {}", i + 1); + } +} + +fn main() { + // `call_me` expects an argument. + call_me(5); +} diff --git a/solutions/02_functions/functions4.rs b/solutions/02_functions/functions4.rs new file mode 100644 index 00000000..f823de24 --- /dev/null +++ b/solutions/02_functions/functions4.rs @@ -0,0 +1,17 @@ +fn is_even(num: i64) -> bool { + num % 2 == 0 +} + +// The return type must always be annotated. +fn sale_price(price: i64) -> i64 { + if is_even(price) { + price - 10 + } else { + price - 3 + } +} + +fn main() { + let original_price = 51; + println!("Your sale price is {}", sale_price(original_price)); +} diff --git a/solutions/02_functions/functions5.rs b/solutions/02_functions/functions5.rs new file mode 100644 index 00000000..677f3278 --- /dev/null +++ b/solutions/02_functions/functions5.rs @@ -0,0 +1,9 @@ +fn square(num: i32) -> i32 { + // Removed the semicolon `;` at the end of the line below to implicitly return the result. + num * num +} + +fn main() { + let answer = square(3); + println!("The square of 3 is {answer}"); +} diff --git a/solutions/03_if/if1.rs b/solutions/03_if/if1.rs new file mode 100644 index 00000000..079c6715 --- /dev/null +++ b/solutions/03_if/if1.rs @@ -0,0 +1,32 @@ +fn bigger(a: i32, b: i32) -> i32 { + if a > b { + a + } else { + b + } +} + +fn main() { + // You can optionally experiment here. +} + +// Don't mind this for now :) +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn ten_is_bigger_than_eight() { + assert_eq!(10, bigger(10, 8)); + } + + #[test] + fn fortytwo_is_bigger_than_thirtytwo() { + assert_eq!(42, bigger(32, 42)); + } + + #[test] + fn equal_numbers() { + assert_eq!(42, bigger(42, 42)); + } +} diff --git a/solutions/03_if/if2.rs b/solutions/03_if/if2.rs new file mode 100644 index 00000000..440bba05 --- /dev/null +++ b/solutions/03_if/if2.rs @@ -0,0 +1,33 @@ +fn foo_if_fizz(fizzish: &str) -> &str { + if fizzish == "fizz" { + "foo" + } else if fizzish == "fuzz" { + "bar" + } else { + "baz" + } +} + +fn main() { + // You can optionally experiment here. +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn foo_for_fizz() { + assert_eq!(foo_if_fizz("fizz"), "foo"); + } + + #[test] + fn bar_for_fuzz() { + assert_eq!(foo_if_fizz("fuzz"), "bar"); + } + + #[test] + fn default_to_baz() { + assert_eq!(foo_if_fizz("literally anything"), "baz"); + } +} diff --git a/solutions/03_if/if3.rs b/solutions/03_if/if3.rs new file mode 100644 index 00000000..571644d4 --- /dev/null +++ b/solutions/03_if/if3.rs @@ -0,0 +1,53 @@ +fn animal_habitat(animal: &str) -> &str { + let identifier = if animal == "crab" { + 1 + } else if animal == "gopher" { + 2 + } else if animal == "snake" { + 3 + } else { + // Any unused identifier. + 4 + }; + + // Instead of such an identifier, you would use an enum in Rust. + // But we didn't get into enums yet. + if identifier == 1 { + "Beach" + } else if identifier == 2 { + "Burrow" + } else if identifier == 3 { + "Desert" + } else { + "Unknown" + } +} + +fn main() { + // You can optionally experiment here. +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn gopher_lives_in_burrow() { + assert_eq!(animal_habitat("gopher"), "Burrow") + } + + #[test] + fn snake_lives_in_desert() { + assert_eq!(animal_habitat("snake"), "Desert") + } + + #[test] + fn crab_lives_on_beach() { + assert_eq!(animal_habitat("crab"), "Beach") + } + + #[test] + fn unknown_animal() { + assert_eq!(animal_habitat("dinosaur"), "Unknown") + } +} diff --git a/solutions/04_primitive_types/primitive_types1.rs b/solutions/04_primitive_types/primitive_types1.rs new file mode 100644 index 00000000..fac6ec04 --- /dev/null +++ b/solutions/04_primitive_types/primitive_types1.rs @@ -0,0 +1,11 @@ +fn main() { + let is_morning = true; + if is_morning { + println!("Good morning!"); + } + + let is_evening = !is_morning; + if is_evening { + println!("Good evening!"); + } +} diff --git a/solutions/04_primitive_types/primitive_types2.rs b/solutions/04_primitive_types/primitive_types2.rs new file mode 100644 index 00000000..eecc6802 --- /dev/null +++ b/solutions/04_primitive_types/primitive_types2.rs @@ -0,0 +1,21 @@ +fn main() { + let my_first_initial = 'C'; + if my_first_initial.is_alphabetic() { + println!("Alphabetical!"); + } else if my_first_initial.is_numeric() { + println!("Numerical!"); + } else { + println!("Neither alphabetic nor numeric!"); + } + + // Example with an emoji. + let your_character = '🦀'; + + if your_character.is_alphabetic() { + println!("Alphabetical!"); + } else if your_character.is_numeric() { + println!("Numerical!"); + } else { + println!("Neither alphabetic nor numeric!"); + } +} diff --git a/solutions/04_primitive_types/primitive_types3.rs b/solutions/04_primitive_types/primitive_types3.rs new file mode 100644 index 00000000..8dd109f9 --- /dev/null +++ b/solutions/04_primitive_types/primitive_types3.rs @@ -0,0 +1,11 @@ +fn main() { + // An array with 100 elements of the value 42. + let a = [42; 100]; + + if a.len() >= 100 { + println!("Wow, that's a big array!"); + } else { + println!("Meh, I eat arrays like that for breakfast."); + panic!("Array not big enough, more elements needed"); + } +} diff --git a/solutions/04_primitive_types/primitive_types4.rs b/solutions/04_primitive_types/primitive_types4.rs new file mode 100644 index 00000000..4807e66c --- /dev/null +++ b/solutions/04_primitive_types/primitive_types4.rs @@ -0,0 +1,23 @@ +fn main() { + // You can optionally experiment here. +} + +#[cfg(test)] +mod tests { + #[test] + fn slice_out_of_array() { + let a = [1, 2, 3, 4, 5]; + // 0 1 2 3 4 <- indices + // ------- + // | + // +--- slice + + // Note that the upper index 4 is excluded. + let nice_slice = &a[1..4]; + assert_eq!([2, 3, 4], nice_slice); + + // The upper index can be included by using the syntax `..=` (with `=` sign) + let nice_slice = &a[1..=3]; + assert_eq!([2, 3, 4], nice_slice); + } +} diff --git a/solutions/04_primitive_types/primitive_types5.rs b/solutions/04_primitive_types/primitive_types5.rs new file mode 100644 index 00000000..46d7ae87 --- /dev/null +++ b/solutions/04_primitive_types/primitive_types5.rs @@ -0,0 +1,8 @@ +fn main() { + let cat = ("Furry McFurson", 3.5); + + // Destructuring the tuple. + let (name, age) = cat; + + println!("{name} is {age} years old"); +} diff --git a/solutions/04_primitive_types/primitive_types6.rs b/solutions/04_primitive_types/primitive_types6.rs new file mode 100644 index 00000000..9b7c2779 --- /dev/null +++ b/solutions/04_primitive_types/primitive_types6.rs @@ -0,0 +1,16 @@ +fn main() { + // You can optionally experiment here. +} + +#[cfg(test)] +mod tests { + #[test] + fn indexing_tuple() { + let numbers = (1, 2, 3); + + // Tuple indexing syntax. + let second = numbers.1; + + assert_eq!(second, 2, "This is not the 2nd number in the tuple!"); + } +} diff --git a/solutions/05_vecs/vecs1.rs b/solutions/05_vecs/vecs1.rs new file mode 100644 index 00000000..55b5676c --- /dev/null +++ b/solutions/05_vecs/vecs1.rs @@ -0,0 +1,23 @@ +fn array_and_vec() -> ([i32; 4], Vec) { + let a = [10, 20, 30, 40]; // Array + + // Used the `vec!` macro. + let v = vec![10, 20, 30, 40]; + + (a, v) +} + +fn main() { + // You can optionally experiment here. +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_array_and_vec_similarity() { + let (a, v) = array_and_vec(); + assert_eq!(a, *v); + } +} diff --git a/solutions/05_vecs/vecs2.rs b/solutions/05_vecs/vecs2.rs new file mode 100644 index 00000000..87f7625a --- /dev/null +++ b/solutions/05_vecs/vecs2.rs @@ -0,0 +1,55 @@ +fn vec_loop(input: &[i32]) -> Vec { + let mut output = Vec::new(); + + for element in input { + output.push(2 * element); + } + + output +} + +fn vec_map_example(input: &[i32]) -> Vec { + // An example of collecting a vector after mapping. + // We map each element of the `input` slice to its value plus 1. + // If the input is `[1, 2, 3]`, the output is `[2, 3, 4]`. + input.iter().map(|element| element + 1).collect() +} + +fn vec_map(input: &[i32]) -> Vec { + // We will dive deeper into iterators, but for now, this is all what you + // had to do! + // Advanced note: This method is more efficient because it automatically + // preallocates enough capacity. This can be done manually in `vec_loop` + // using `Vec::with_capacity(input.len())` instead of `Vec::new()`. + input.iter().map(|element| 2 * element).collect() +} + +fn main() { + // You can optionally experiment here. +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_vec_loop() { + let input = [2, 4, 6, 8, 10]; + let ans = vec_loop(&input); + assert_eq!(ans, [4, 8, 12, 16, 20]); + } + + #[test] + fn test_vec_map_example() { + let input = [1, 2, 3]; + let ans = vec_map_example(&input); + assert_eq!(ans, [2, 3, 4]); + } + + #[test] + fn test_vec_map() { + let input = [2, 4, 6, 8, 10]; + let ans = vec_map(&input); + assert_eq!(ans, [4, 8, 12, 16, 20]); + } +} diff --git a/solutions/06_move_semantics/move_semantics1.rs b/solutions/06_move_semantics/move_semantics1.rs new file mode 100644 index 00000000..ac34e7a0 --- /dev/null +++ b/solutions/06_move_semantics/move_semantics1.rs @@ -0,0 +1,25 @@ +fn fill_vec(vec: Vec) -> Vec { + let mut vec = vec; + // ^^^ added + + vec.push(88); + + vec +} + +fn main() { + // You can optionally experiment here. +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn move_semantics1() { + let vec0 = vec![22, 44, 66]; + let vec1 = fill_vec(vec0); + // `vec0` can't be accessed anymore because it is moved to `fill_vec`. + assert_eq!(vec1, vec![22, 44, 66, 88]); + } +} diff --git a/solutions/06_move_semantics/move_semantics2.rs b/solutions/06_move_semantics/move_semantics2.rs new file mode 100644 index 00000000..7bcd33a0 --- /dev/null +++ b/solutions/06_move_semantics/move_semantics2.rs @@ -0,0 +1,28 @@ +fn fill_vec(vec: Vec) -> Vec { + let mut vec = vec; + + vec.push(88); + + vec +} + +fn main() { + // You can optionally experiment here. +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn move_semantics2() { + let vec0 = vec![22, 44, 66]; + + // Cloning `vec0` so that the clone is moved into `fill_vec`, not `vec0` + // itself. + let vec1 = fill_vec(vec0.clone()); + + assert_eq!(vec0, [22, 44, 66]); + assert_eq!(vec1, [22, 44, 66, 88]); + } +} diff --git a/solutions/06_move_semantics/move_semantics3.rs b/solutions/06_move_semantics/move_semantics3.rs new file mode 100644 index 00000000..7ba4006b --- /dev/null +++ b/solutions/06_move_semantics/move_semantics3.rs @@ -0,0 +1,22 @@ +fn fill_vec(mut vec: Vec) -> Vec { + // ^^^ added + vec.push(88); + + vec +} + +fn main() { + // You can optionally experiment here. +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn move_semantics3() { + let vec0 = vec![22, 44, 66]; + let vec1 = fill_vec(vec0); + assert_eq!(vec1, [22, 44, 66, 88]); + } +} diff --git a/solutions/06_move_semantics/move_semantics4.rs b/solutions/06_move_semantics/move_semantics4.rs new file mode 100644 index 00000000..b7919ac0 --- /dev/null +++ b/solutions/06_move_semantics/move_semantics4.rs @@ -0,0 +1,21 @@ +fn main() { + // You can optionally experiment here. +} + +#[cfg(test)] +mod tests { + // TODO: Fix the compiler errors only by reordering the lines in the test. + // Don't add, change or remove any line. + #[test] + fn move_semantics5() { + let mut x = 100; + let y = &mut x; + // `y` used here. + *y += 100; + // The mutable reference `y` is not used anymore, + // therefore a new reference can be created. + let z = &mut x; + *z += 1000; + assert_eq!(x, 1200); + } +} diff --git a/solutions/06_move_semantics/move_semantics5.rs b/solutions/06_move_semantics/move_semantics5.rs new file mode 100644 index 00000000..1b3ca4eb --- /dev/null +++ b/solutions/06_move_semantics/move_semantics5.rs @@ -0,0 +1,21 @@ +fn main() { + let data = "Rust is great!".to_string(); + + get_char(&data); + + string_uppercase(data); +} + +// Borrows instead of taking ownership. +// It is recommended to use `&str` instead of `&String` here. But this is +// enough for now because we didn't handle strings yet. +fn get_char(data: &String) -> char { + data.chars().last().unwrap() +} + +// Takes ownership instead of borrowing. +fn string_uppercase(mut data: String) { + data = data.to_uppercase(); + + println!("{data}"); +} diff --git a/solutions/07_structs/structs1.rs b/solutions/07_structs/structs1.rs new file mode 100644 index 00000000..98fafcc5 --- /dev/null +++ b/solutions/07_structs/structs1.rs @@ -0,0 +1,49 @@ +struct ColorRegularStruct { + red: u8, + green: u8, + blue: u8, +} + +struct ColorTupleStruct(u8, u8, u8); + +#[derive(Debug)] +struct UnitStruct; + +fn main() { + // You can optionally experiment here. +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn regular_structs() { + let green = ColorRegularStruct { + red: 0, + green: 255, + blue: 0, + }; + + assert_eq!(green.red, 0); + assert_eq!(green.green, 255); + assert_eq!(green.blue, 0); + } + + #[test] + fn tuple_structs() { + let green = ColorTupleStruct(0, 255, 0); + + assert_eq!(green.0, 0); + assert_eq!(green.1, 255); + assert_eq!(green.2, 0); + } + + #[test] + fn unit_structs() { + let unit_struct = UnitStruct; + let message = format!("{unit_struct:?}s are fun!"); + + assert_eq!(message, "UnitStructs are fun!"); + } +} diff --git a/solutions/07_structs/structs2.rs b/solutions/07_structs/structs2.rs new file mode 100644 index 00000000..589dd933 --- /dev/null +++ b/solutions/07_structs/structs2.rs @@ -0,0 +1,51 @@ +#[derive(Debug)] +struct Order { + name: String, + year: u32, + made_by_phone: bool, + made_by_mobile: bool, + made_by_email: bool, + item_number: u32, + count: u32, +} + +fn create_order_template() -> Order { + Order { + name: String::from("Bob"), + year: 2019, + made_by_phone: false, + made_by_mobile: false, + made_by_email: true, + item_number: 123, + count: 0, + } +} + +fn main() { + // You can optionally experiment here. +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn your_order() { + let order_template = create_order_template(); + + let your_order = Order { + name: String::from("Hacker in Rust"), + count: 1, + // Struct update syntax + ..order_template + }; + + assert_eq!(your_order.name, "Hacker in Rust"); + assert_eq!(your_order.year, order_template.year); + assert_eq!(your_order.made_by_phone, order_template.made_by_phone); + assert_eq!(your_order.made_by_mobile, order_template.made_by_mobile); + assert_eq!(your_order.made_by_email, order_template.made_by_email); + assert_eq!(your_order.item_number, order_template.item_number); + assert_eq!(your_order.count, 1); + } +} diff --git a/solutions/07_structs/structs3.rs b/solutions/07_structs/structs3.rs new file mode 100644 index 00000000..3f878cc8 --- /dev/null +++ b/solutions/07_structs/structs3.rs @@ -0,0 +1,83 @@ +#[derive(Debug)] +struct Package { + sender_country: String, + recipient_country: String, + weight_in_grams: u32, +} + +impl Package { + fn new(sender_country: String, recipient_country: String, weight_in_grams: u32) -> Self { + if weight_in_grams < 10 { + // This isn't how you should handle errors in Rust, but we will + // learn about error handling later. + panic!("Can't ship a package with weight below 10 grams"); + } + + Self { + sender_country, + recipient_country, + weight_in_grams, + } + } + + fn is_international(&self) -> bool { + // ^^^^^^^ added + self.sender_country != self.recipient_country + } + + fn get_fees(&self, cents_per_gram: u32) -> u32 { + // ^^^^^^ added + self.weight_in_grams * cents_per_gram + } +} + +fn main() { + // You can optionally experiment here. +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + #[should_panic] + fn fail_creating_weightless_package() { + let sender_country = String::from("Spain"); + let recipient_country = String::from("Austria"); + + Package::new(sender_country, recipient_country, 5); + } + + #[test] + fn create_international_package() { + let sender_country = String::from("Spain"); + let recipient_country = String::from("Russia"); + + let package = Package::new(sender_country, recipient_country, 1200); + + assert!(package.is_international()); + } + + #[test] + fn create_local_package() { + let sender_country = String::from("Canada"); + let recipient_country = sender_country.clone(); + + let package = Package::new(sender_country, recipient_country, 1200); + + assert!(!package.is_international()); + } + + #[test] + fn calculate_transport_fees() { + let sender_country = String::from("Spain"); + let recipient_country = String::from("Spain"); + + let cents_per_gram = 3; + + let package = Package::new(sender_country, recipient_country, 1500); + + assert_eq!(package.get_fees(cents_per_gram), 4500); + assert_eq!(package.get_fees(cents_per_gram * 2), 9000); + } +} diff --git a/solutions/08_enums/enums1.rs b/solutions/08_enums/enums1.rs new file mode 100644 index 00000000..97248834 --- /dev/null +++ b/solutions/08_enums/enums1.rs @@ -0,0 +1,14 @@ +#[derive(Debug)] +enum Message { + Quit, + Echo, + Move, + ChangeColor, +} + +fn main() { + println!("{:?}", Message::Quit); + println!("{:?}", Message::Echo); + println!("{:?}", Message::Move); + println!("{:?}", Message::ChangeColor); +} diff --git a/solutions/08_enums/enums2.rs b/solutions/08_enums/enums2.rs new file mode 100644 index 00000000..13175dd3 --- /dev/null +++ b/solutions/08_enums/enums2.rs @@ -0,0 +1,26 @@ +#[derive(Debug)] +enum Message { + Move { x: i64, y: i64 }, + Echo(String), + ChangeColor(u8, u8, u8), + Quit, +} + +impl Message { + fn call(&self) { + println!("{:?}", self); + } +} + +fn main() { + let messages = [ + Message::Move { x: 10, y: 30 }, + Message::Echo(String::from("hello world")), + Message::ChangeColor(200, 255, 255), + Message::Quit, + ]; + + for message in &messages { + message.call(); + } +} diff --git a/solutions/08_enums/enums3.rs b/solutions/08_enums/enums3.rs new file mode 100644 index 00000000..8baa25c1 --- /dev/null +++ b/solutions/08_enums/enums3.rs @@ -0,0 +1,75 @@ +enum Message { + ChangeColor(u8, u8, u8), + Echo(String), + Move(Point), + Quit, +} + +struct Point { + x: u8, + y: u8, +} + +struct State { + color: (u8, u8, u8), + position: Point, + quit: bool, + message: String, +} + +impl State { + fn change_color(&mut self, color: (u8, u8, u8)) { + self.color = color; + } + + fn quit(&mut self) { + self.quit = true; + } + + fn echo(&mut self, s: String) { + self.message = s; + } + + fn move_position(&mut self, point: Point) { + self.position = point; + } + + fn process(&mut self, message: Message) { + match message { + Message::ChangeColor(r, g, b) => self.change_color((r, g, b)), + Message::Echo(s) => self.echo(s), + Message::Move(point) => self.move_position(point), + Message::Quit => self.quit(), + } + } +} + +fn main() { + // You can optionally experiment here. +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_match_message_call() { + let mut state = State { + quit: false, + position: Point { x: 0, y: 0 }, + color: (0, 0, 0), + message: String::from("hello world"), + }; + + state.process(Message::ChangeColor(255, 0, 255)); + state.process(Message::Echo(String::from("Hello world!"))); + state.process(Message::Move(Point { x: 10, y: 15 })); + state.process(Message::Quit); + + assert_eq!(state.color, (255, 0, 255)); + assert_eq!(state.position.x, 10); + assert_eq!(state.position.y, 15); + assert!(state.quit); + assert_eq!(state.message, "Hello world!"); + } +} diff --git a/solutions/09_strings/strings1.rs b/solutions/09_strings/strings1.rs new file mode 100644 index 00000000..f7ba8114 --- /dev/null +++ b/solutions/09_strings/strings1.rs @@ -0,0 +1,9 @@ +fn current_favorite_color() -> String { + // Equivalent to `String::from("blue")` + "blue".to_string() +} + +fn main() { + let answer = current_favorite_color(); + println!("My current favorite color is {answer}"); +} diff --git a/solutions/09_strings/strings2.rs b/solutions/09_strings/strings2.rs new file mode 100644 index 00000000..7de311ff --- /dev/null +++ b/solutions/09_strings/strings2.rs @@ -0,0 +1,15 @@ +fn is_a_color_word(attempt: &str) -> bool { + attempt == "green" || attempt == "blue" || attempt == "red" +} + +fn main() { + let word = String::from("green"); + + if is_a_color_word(&word) { + // ^ added to have `&String` which is automatically + // coerced to `&str` by the compiler. + println!("That is a color word I know!"); + } else { + println!("That is not a color word I know."); + } +} diff --git a/solutions/09_strings/strings3.rs b/solutions/09_strings/strings3.rs new file mode 100644 index 00000000..a478e62a --- /dev/null +++ b/solutions/09_strings/strings3.rs @@ -0,0 +1,48 @@ +fn trim_me(input: &str) -> &str { + input.trim() +} + +fn compose_me(input: &str) -> String { + // The macro `format!` has the same syntax as `println!`, but it returns a + // string instead of printing it to the terminal. + // Equivalent to `input.to_string() + " world!"` + format!("{input} world!") +} + +fn replace_me(input: &str) -> String { + input.replace("cars", "balloons") +} + +fn main() { + // You can optionally experiment here. +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn trim_a_string() { + assert_eq!(trim_me("Hello! "), "Hello!"); + assert_eq!(trim_me(" What's up!"), "What's up!"); + assert_eq!(trim_me(" Hola! "), "Hola!"); + } + + #[test] + fn compose_a_string() { + assert_eq!(compose_me("Hello"), "Hello world!"); + assert_eq!(compose_me("Goodbye"), "Goodbye world!"); + } + + #[test] + fn replace_a_string() { + assert_eq!( + replace_me("I think cars are cool"), + "I think balloons are cool", + ); + assert_eq!( + replace_me("I love to look at cars"), + "I love to look at balloons", + ); + } +} diff --git a/solutions/09_strings/strings4.rs b/solutions/09_strings/strings4.rs new file mode 100644 index 00000000..9dc6917e --- /dev/null +++ b/solutions/09_strings/strings4.rs @@ -0,0 +1,38 @@ +fn string_slice(arg: &str) { + println!("{arg}"); +} +fn string(arg: String) { + println!("{arg}"); +} + +fn main() { + string_slice("blue"); + + string("red".to_string()); + + string(String::from("hi")); + + string("rust is fun!".to_owned()); + + // Here, both answers work. + // `.into()` converts a type into an expected type. + // If it is called where `String` is expected, it will convert `&str` to `String`. + // But if is called where `&str` is expected, then `&str` is kept `&str` since no + // conversion is needed. + string("nice weather".into()); + string_slice("nice weather".into()); + // ^^^^^^^ the compiler recommends removing the `.into()` + // call because it is a useless conversion. + + string(format!("Interpolation {}", "Station")); + + // WARNING: This is byte indexing, not character indexing. + // Character indexing can be done using `s.chars().nth(INDEX)`. + string_slice(&String::from("abc")[0..1]); + + string_slice(" hello there ".trim()); + + string("Happy Monday!".replace("Mon", "Tues")); + + string("mY sHiFt KeY iS sTiCkY".to_lowercase()); +} diff --git a/solutions/10_modules/modules1.rs b/solutions/10_modules/modules1.rs new file mode 100644 index 00000000..873b4127 --- /dev/null +++ b/solutions/10_modules/modules1.rs @@ -0,0 +1,15 @@ +mod sausage_factory { + fn get_secret_recipe() -> String { + String::from("Ginger") + } + + // Added `pub` before `fn` to make the function accessible outside the module. + pub fn make_sausage() { + get_secret_recipe(); + println!("sausage!"); + } +} + +fn main() { + sausage_factory::make_sausage(); +} diff --git a/solutions/10_modules/modules2.rs b/solutions/10_modules/modules2.rs new file mode 100644 index 00000000..55c316d7 --- /dev/null +++ b/solutions/10_modules/modules2.rs @@ -0,0 +1,23 @@ +mod delicious_snacks { + // Added `pub` and used the expected alias after `as`. + pub use self::fruits::PEAR as fruit; + pub use self::veggies::CUCUMBER as veggie; + + mod fruits { + pub const PEAR: &str = "Pear"; + pub const APPLE: &str = "Apple"; + } + + mod veggies { + pub const CUCUMBER: &str = "Cucumber"; + pub const CARROT: &str = "Carrot"; + } +} + +fn main() { + println!( + "favorite snacks: {} and {}", + delicious_snacks::fruit, + delicious_snacks::veggie, + ); +} diff --git a/solutions/10_modules/modules3.rs b/solutions/10_modules/modules3.rs new file mode 100644 index 00000000..99ff5a77 --- /dev/null +++ b/solutions/10_modules/modules3.rs @@ -0,0 +1,8 @@ +use std::time::{SystemTime, UNIX_EPOCH}; + +fn main() { + match SystemTime::now().duration_since(UNIX_EPOCH) { + Ok(n) => println!("1970-01-01 00:00:00 UTC was {} seconds ago!", n.as_secs()), + Err(_) => panic!("SystemTime before UNIX EPOCH!"), + } +} diff --git a/solutions/11_hashmaps/hashmaps1.rs b/solutions/11_hashmaps/hashmaps1.rs new file mode 100644 index 00000000..3a787c43 --- /dev/null +++ b/solutions/11_hashmaps/hashmaps1.rs @@ -0,0 +1,42 @@ +// A basket of fruits in the form of a hash map needs to be defined. The key +// represents the name of the fruit and the value represents how many of that +// particular fruit is in the basket. You have to put at least 3 different +// types of fruits (e.g apple, banana, mango) in the basket and the total count +// of all the fruits should be at least 5. + +use std::collections::HashMap; + +fn fruit_basket() -> HashMap { + // Declare the hash map. + let mut basket = HashMap::new(); + + // Two bananas are already given for you :) + basket.insert(String::from("banana"), 2); + + // Put more fruits in your basket. + basket.insert(String::from("apple"), 3); + basket.insert(String::from("mango"), 1); + + basket +} + +fn main() { + // You can optionally experiment here. +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn at_least_three_types_of_fruits() { + let basket = fruit_basket(); + assert!(basket.len() >= 3); + } + + #[test] + fn at_least_five_fruits() { + let basket = fruit_basket(); + assert!(basket.values().sum::() >= 5); + } +} diff --git a/solutions/11_hashmaps/hashmaps2.rs b/solutions/11_hashmaps/hashmaps2.rs new file mode 100644 index 00000000..a5e6ef9b --- /dev/null +++ b/solutions/11_hashmaps/hashmaps2.rs @@ -0,0 +1,95 @@ +// We're collecting different fruits to bake a delicious fruit cake. For this, +// we have a basket, which we'll represent in the form of a hash map. The key +// represents the name of each fruit we collect and the value represents how +// many of that particular fruit we have collected. Three types of fruits - +// Apple (4), Mango (2) and Lychee (5) are already in the basket hash map. You +// must add fruit to the basket so that there is at least one of each kind and +// more than 11 in total - we have a lot of mouths to feed. You are not allowed +// to insert any more of these fruits! + +use std::collections::HashMap; + +#[derive(Hash, PartialEq, Eq, Debug)] +enum Fruit { + Apple, + Banana, + Mango, + Lychee, + Pineapple, +} + +fn fruit_basket(basket: &mut HashMap) { + let fruit_kinds = [ + Fruit::Apple, + Fruit::Banana, + Fruit::Mango, + Fruit::Lychee, + Fruit::Pineapple, + ]; + + for fruit in fruit_kinds { + // If fruit doesn't exist, insert it with some value. + basket.entry(fruit).or_insert(5); + } +} + +fn main() { + // You can optionally experiment here. +} + +#[cfg(test)] +mod tests { + use super::*; + + // Don't modify this function! + fn get_fruit_basket() -> HashMap { + let content = [(Fruit::Apple, 4), (Fruit::Mango, 2), (Fruit::Lychee, 5)]; + HashMap::from_iter(content) + } + + #[test] + fn test_given_fruits_are_not_modified() { + let mut basket = get_fruit_basket(); + fruit_basket(&mut basket); + assert_eq!(*basket.get(&Fruit::Apple).unwrap(), 4); + assert_eq!(*basket.get(&Fruit::Mango).unwrap(), 2); + assert_eq!(*basket.get(&Fruit::Lychee).unwrap(), 5); + } + + #[test] + fn at_least_five_types_of_fruits() { + let mut basket = get_fruit_basket(); + fruit_basket(&mut basket); + let count_fruit_kinds = basket.len(); + assert!(count_fruit_kinds >= 5); + } + + #[test] + fn greater_than_eleven_fruits() { + let mut basket = get_fruit_basket(); + fruit_basket(&mut basket); + let count = basket.values().sum::(); + assert!(count > 11); + } + + #[test] + fn all_fruit_types_in_basket() { + let fruit_kinds = [ + Fruit::Apple, + Fruit::Banana, + Fruit::Mango, + Fruit::Lychee, + Fruit::Pineapple, + ]; + + let mut basket = get_fruit_basket(); + fruit_basket(&mut basket); + + for fruit_kind in fruit_kinds { + let Some(amount) = basket.get(&fruit_kind) else { + panic!("Fruit kind {fruit_kind:?} was not found in basket"); + }; + assert!(*amount > 0); + } + } +} diff --git a/solutions/11_hashmaps/hashmaps3.rs b/solutions/11_hashmaps/hashmaps3.rs new file mode 100644 index 00000000..f4059bbe --- /dev/null +++ b/solutions/11_hashmaps/hashmaps3.rs @@ -0,0 +1,83 @@ +// A list of scores (one per line) of a soccer match is given. Each line is of +// the form ",,," +// Example: "England,France,4,2" (England scored 4 goals, France 2). +// +// You have to build a scores table containing the name of the team, the total +// number of goals the team scored, and the total number of goals the team +// conceded. + +use std::collections::HashMap; + +// A structure to store the goal details of a team. +#[derive(Default)] +struct Team { + goals_scored: u8, + goals_conceded: u8, +} + +fn build_scores_table(results: &str) -> HashMap<&str, Team> { + // The name of the team is the key and its associated struct is the value. + let mut scores = HashMap::new(); + + for line in results.lines() { + let mut split_iterator = line.split(','); + // NOTE: We use `unwrap` because we didn't deal with error handling yet. + let team_1_name = split_iterator.next().unwrap(); + let team_2_name = split_iterator.next().unwrap(); + let team_1_score: u8 = split_iterator.next().unwrap().parse().unwrap(); + let team_2_score: u8 = split_iterator.next().unwrap().parse().unwrap(); + + // Insert the default with zeros if a team doesn't exist yet. + let mut team_1 = scores.entry(team_1_name).or_insert_with(|| Team::default()); + // Update the values. + team_1.goals_scored += team_1_score; + team_1.goals_conceded += team_2_score; + + // Similarely for the second team. + let mut team_2 = scores.entry(team_2_name).or_insert_with(|| Team::default()); + team_2.goals_scored += team_2_score; + team_2.goals_conceded += team_1_score; + } + + scores +} + +fn main() { + // You can optionally experiment here. +} + +#[cfg(test)] +mod tests { + use super::*; + + const RESULTS: &str = "England,France,4,2 +France,Italy,3,1 +Poland,Spain,2,0 +Germany,England,2,1 +England,Spain,1,0"; + + #[test] + fn build_scores() { + let scores = build_scores_table(RESULTS); + + assert!(["England", "France", "Germany", "Italy", "Poland", "Spain"] + .into_iter() + .all(|team_name| scores.contains_key(team_name))); + } + + #[test] + fn validate_team_score_1() { + let scores = build_scores_table(RESULTS); + let team = scores.get("England").unwrap(); + assert_eq!(team.goals_scored, 6); + assert_eq!(team.goals_conceded, 4); + } + + #[test] + fn validate_team_score_2() { + let scores = build_scores_table(RESULTS); + let team = scores.get("Spain").unwrap(); + assert_eq!(team.goals_scored, 0); + assert_eq!(team.goals_conceded, 3); + } +} diff --git a/solutions/12_options/options1.rs b/solutions/12_options/options1.rs new file mode 100644 index 00000000..1ffbb045 --- /dev/null +++ b/solutions/12_options/options1.rs @@ -0,0 +1,38 @@ +// This function returns how much icecream there is left in the fridge. +// If it's before 22:00 (24-hour system), then 5 scoops are left. At 22:00, +// someone eats it all, so no icecream is left (value 0). Return `None` if +// `hour_of_day` is higher than 23. +fn maybe_icecream(hour_of_day: u16) -> Option { + match hour_of_day { + 0..22 => Some(5), + 22..24 => Some(0), + _ => None, + } +} + +fn main() { + // You can optionally experiment here. +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn raw_value() { + // Using `unwrap` is fine in a test. + let icecreams = maybe_icecream(12).unwrap(); + assert_eq!(icecreams, 5); + } + + #[test] + fn check_icecream() { + assert_eq!(maybe_icecream(0), Some(5)); + assert_eq!(maybe_icecream(9), Some(5)); + assert_eq!(maybe_icecream(18), Some(5)); + assert_eq!(maybe_icecream(22), Some(0)); + assert_eq!(maybe_icecream(23), Some(0)); + assert_eq!(maybe_icecream(24), None); + assert_eq!(maybe_icecream(25), None); + } +} diff --git a/solutions/12_options/options2.rs b/solutions/12_options/options2.rs new file mode 100644 index 00000000..0f24665f --- /dev/null +++ b/solutions/12_options/options2.rs @@ -0,0 +1,37 @@ +fn main() { + // You can optionally experiment here. +} + +#[cfg(test)] +mod tests { + #[test] + fn simple_option() { + let target = "rustlings"; + let optional_target = Some(target); + + // if-let + if let Some(word) = optional_target { + assert_eq!(word, target); + } + } + + #[test] + fn layered_option() { + let range = 10; + let mut optional_integers: Vec> = vec![None]; + + for i in 1..=range { + optional_integers.push(Some(i)); + } + + let mut cursor = range; + + // while-let with nested pattern matching + while let Some(Some(integer)) = optional_integers.pop() { + assert_eq!(integer, cursor); + cursor -= 1; + } + + assert_eq!(cursor, 0); + } +} diff --git a/solutions/12_options/options3.rs b/solutions/12_options/options3.rs new file mode 100644 index 00000000..0081eeb2 --- /dev/null +++ b/solutions/12_options/options3.rs @@ -0,0 +1,26 @@ +#[derive(Debug)] +struct Point { + x: i32, + y: i32, +} + +fn main() { + let optional_point = Some(Point { x: 100, y: 200 }); + + // Solution 1: Matching over the `Option` (not `&Option`) but without moving + // out of the `Some` variant. + match optional_point { + Some(ref p) => println!("Co-ordinates are {},{}", p.x, p.y), + // ^^^ added + _ => panic!("No match!"), + } + + // Solution 2: Matching over a reference (`&Option`) by added `&` before + // `optional_point`. + match &optional_point { + Some(p) => println!("Co-ordinates are {},{}", p.x, p.y), + _ => panic!("No match!"), + } + + println!("{optional_point:?}"); +} diff --git a/solutions/13_error_handling/errors1.rs b/solutions/13_error_handling/errors1.rs new file mode 100644 index 00000000..f552ca7f --- /dev/null +++ b/solutions/13_error_handling/errors1.rs @@ -0,0 +1,37 @@ +fn generate_nametag_text(name: String) -> Result { + // ^^^^^^ ^^^^^^ + if name.is_empty() { + // `Err(String)` instead of `None`. + Err("Empty names aren't allowed".to_string()) + } else { + // `Ok` instead of `Some`. + Ok(format!("Hi! My name is {name}")) + } +} + +fn main() { + // You can optionally experiment here. +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn generates_nametag_text_for_a_nonempty_name() { + assert_eq!( + generate_nametag_text("Beyoncé".to_string()).as_deref(), + Ok("Hi! My name is Beyoncé"), + ); + } + + #[test] + fn explains_why_generating_nametag_text_fails() { + assert_eq!( + generate_nametag_text(String::new()) + .as_ref() + .map_err(|e| e.as_str()), + Err("Empty names aren't allowed"), + ); + } +} diff --git a/solutions/13_error_handling/errors2.rs b/solutions/13_error_handling/errors2.rs new file mode 100644 index 00000000..de7c32b5 --- /dev/null +++ b/solutions/13_error_handling/errors2.rs @@ -0,0 +1,57 @@ +// Say we're writing a game where you can buy items with tokens. All items cost +// 5 tokens, and whenever you purchase items there is a processing fee of 1 +// token. A player of the game will type in how many items they want to buy, and +// the `total_cost` function will calculate the total cost of the items. Since +// the player typed in the quantity, we get it as a string. They might have +// typed anything, not just numbers! +// +// Right now, this function isn't handling the error case at all (and isn't +// handling the success case properly either). What we want to do is: If we call +// the `total_cost` function on a string that is not a number, that function +// will return a `ParseIntError`. In that case, we want to immediately return +// that error from our function and not try to multiply and add. +// +// There are at least two ways to implement this that are both correct. But one +// is a lot shorter! + +use std::num::ParseIntError; + +fn total_cost(item_quantity: &str) -> Result { + let processing_fee = 1; + let cost_per_item = 5; + + // Added `?` to propagate the error. + let qty = item_quantity.parse::()?; + // ^ added + + // Equivalent to this verbose version: + let qty = match item_quantity.parse::() { + Ok(v) => v, + Err(e) => return Err(e), + }; + + Ok(qty * cost_per_item + processing_fee) +} + +fn main() { + // You can optionally experiment here. +} + +#[cfg(test)] +mod tests { + use super::*; + use std::num::IntErrorKind; + + #[test] + fn item_quantity_is_a_valid_number() { + assert_eq!(total_cost("34"), Ok(171)); + } + + #[test] + fn item_quantity_is_an_invalid_number() { + assert_eq!( + total_cost("beep boop").unwrap_err().kind(), + &IntErrorKind::InvalidDigit, + ); + } +} diff --git a/solutions/13_error_handling/errors3.rs b/solutions/13_error_handling/errors3.rs new file mode 100644 index 00000000..63f4aba1 --- /dev/null +++ b/solutions/13_error_handling/errors3.rs @@ -0,0 +1,32 @@ +// This is a program that is trying to use a completed version of the +// `total_cost` function from the previous exercise. It's not working though! +// Why not? What should we do to fix it? + +use std::num::ParseIntError; + +// Don't change this function. +fn total_cost(item_quantity: &str) -> Result { + let processing_fee = 1; + let cost_per_item = 5; + let qty = item_quantity.parse::()?; + + Ok(qty * cost_per_item + processing_fee) +} + +fn main() -> Result<(), ParseIntError> { + // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ added + let mut tokens = 100; + let pretend_user_input = "8"; + + let cost = total_cost(pretend_user_input)?; + + if cost > tokens { + println!("You can't afford that many!"); + } else { + tokens -= cost; + println!("You now have {tokens} tokens."); + } + + // Added this line to return the `Ok` variant of the expected `Result`. + Ok(()) +} diff --git a/solutions/13_error_handling/errors4.rs b/solutions/13_error_handling/errors4.rs new file mode 100644 index 00000000..c43f493b --- /dev/null +++ b/solutions/13_error_handling/errors4.rs @@ -0,0 +1,42 @@ +#[derive(PartialEq, Debug)] +enum CreationError { + Negative, + Zero, +} + +#[derive(PartialEq, Debug)] +struct PositiveNonzeroInteger(u64); + +impl PositiveNonzeroInteger { + fn new(value: i64) -> Result { + if value == 0 { + Err(CreationError::Zero) + } else if value < 0 { + Err(CreationError::Negative) + } else { + Ok(Self(value as u64)) + } + } +} + +fn main() { + // You can optionally experiment here. +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_creation() { + assert_eq!( + PositiveNonzeroInteger::new(10), + Ok(PositiveNonzeroInteger(10)), + ); + assert_eq!( + PositiveNonzeroInteger::new(-10), + Err(CreationError::Negative), + ); + assert_eq!(PositiveNonzeroInteger::new(0), Err(CreationError::Zero)); + } +} diff --git a/solutions/13_error_handling/errors5.rs b/solutions/13_error_handling/errors5.rs new file mode 100644 index 00000000..c1424eee --- /dev/null +++ b/solutions/13_error_handling/errors5.rs @@ -0,0 +1,54 @@ +// This exercise is an altered version of the `errors4` exercise. It uses some +// concepts that we won't get to until later in the course, like `Box` and the +// `From` trait. It's not important to understand them in detail right now, but +// you can read ahead if you like. For now, think of the `Box` type as +// an "I want anything that does ???" type. +// +// In short, this particular use case for boxes is for when you want to own a +// value and you care only that it is a type which implements a particular +// trait. To do so, The `Box` is declared as of type `Box` where +// `Trait` is the trait the compiler looks for on any value used in that +// context. For this exercise, that context is the potential errors which +// can be returned in a `Result`. + +use std::error::Error; +use std::fmt; + +#[derive(PartialEq, Debug)] +enum CreationError { + Negative, + Zero, +} + +// This is required so that `CreationError` can implement `Error`. +impl fmt::Display for CreationError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let description = match *self { + CreationError::Negative => "number is negative", + CreationError::Zero => "number is zero", + }; + f.write_str(description) + } +} + +impl Error for CreationError {} + +#[derive(PartialEq, Debug)] +struct PositiveNonzeroInteger(u64); + +impl PositiveNonzeroInteger { + fn new(value: i64) -> Result { + match value { + x if x < 0 => Err(CreationError::Negative), + 0 => Err(CreationError::Zero), + x => Ok(PositiveNonzeroInteger(x as u64)), + } + } +} + +fn main() -> Result<(), Box> { + let pretend_user_input = "42"; + let x: i64 = pretend_user_input.parse()?; + println!("output={:?}", PositiveNonzeroInteger::new(x)?); + Ok(()) +} diff --git a/solutions/13_error_handling/errors6.rs b/solutions/13_error_handling/errors6.rs new file mode 100644 index 00000000..70680cf0 --- /dev/null +++ b/solutions/13_error_handling/errors6.rs @@ -0,0 +1,92 @@ +// Using catch-all error types like `Box` isn't recommended for +// library code where callers might want to make decisions based on the error +// content instead of printing it out or propagating it further. Here, we define +// a custom error type to make it possible for callers to decide what to do next +// when our function returns an error. + +use std::num::ParseIntError; + +#[derive(PartialEq, Debug)] +enum CreationError { + Negative, + Zero, +} + +// A custom error type that we will be using in `PositiveNonzeroInteger::parse`. +#[derive(PartialEq, Debug)] +enum ParsePosNonzeroError { + Creation(CreationError), + ParseInt(ParseIntError), +} + +impl ParsePosNonzeroError { + fn from_creation(err: CreationError) -> Self { + Self::Creation(err) + } + + fn from_parseint(err: ParseIntError) -> Self { + Self::ParseInt(err) + } +} + +#[derive(PartialEq, Debug)] +struct PositiveNonzeroInteger(u64); + +impl PositiveNonzeroInteger { + fn new(value: i64) -> Result { + match value { + x if x < 0 => Err(CreationError::Negative), + x if x == 0 => Err(CreationError::Zero), + x => Ok(Self(x as u64)), + } + } + + fn parse(s: &str) -> Result { + // Return an appropriate error instead of panicking when `parse()` + // returns an error. + let x: i64 = s.parse().map_err(ParsePosNonzeroError::from_parseint)?; + // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + Self::new(x).map_err(ParsePosNonzeroError::from_creation) + } +} + +fn main() { + // You can optionally experiment here. +} + +#[cfg(test)] +mod test { + use super::*; + use std::num::IntErrorKind; + + #[test] + fn test_parse_error() { + assert!(matches!( + PositiveNonzeroInteger::parse("not a number"), + Err(ParsePosNonzeroError::ParseInt(_)), + )); + } + + #[test] + fn test_negative() { + assert_eq!( + PositiveNonzeroInteger::parse("-555"), + Err(ParsePosNonzeroError::Creation(CreationError::Negative)), + ); + } + + #[test] + fn test_zero() { + assert_eq!( + PositiveNonzeroInteger::parse("0"), + Err(ParsePosNonzeroError::Creation(CreationError::Zero)), + ); + } + + #[test] + fn test_positive() { + let x = PositiveNonzeroInteger::new(42).unwrap(); + assert_eq!(x.0, 42); + assert_eq!(PositiveNonzeroInteger::parse("42"), Ok(x)); + } +} diff --git a/solutions/14_generics/generics1.rs b/solutions/14_generics/generics1.rs new file mode 100644 index 00000000..e2195fd3 --- /dev/null +++ b/solutions/14_generics/generics1.rs @@ -0,0 +1,17 @@ +// `Vec` is generic over the type `T`. In most cases, the compiler is able to +// infer `T`, for example after pushing a value with a concrete type to the vector. +// But in this exercise, the compiler needs some help through a type annotation. + +fn main() { + // `u8` and `i8` can both be converted to `i16`. + let mut numbers: Vec = Vec::new(); + // ^^^^^^^^^^ added + + // Don't change the lines below. + let n1: u8 = 42; + numbers.push(n1.into()); + let n2: i8 = -1; + numbers.push(n2.into()); + + println!("{numbers:?}"); +} diff --git a/solutions/14_generics/generics2.rs b/solutions/14_generics/generics2.rs new file mode 100644 index 00000000..14f3f7af --- /dev/null +++ b/solutions/14_generics/generics2.rs @@ -0,0 +1,28 @@ +struct Wrapper { + value: T, +} + +impl Wrapper { + fn new(value: T) -> Self { + Wrapper { value } + } +} + +fn main() { + // You can optionally experiment here. +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn store_u32_in_wrapper() { + assert_eq!(Wrapper::new(42).value, 42); + } + + #[test] + fn store_str_in_wrapper() { + assert_eq!(Wrapper::new("Foo").value, "Foo"); + } +} diff --git a/solutions/15_traits/traits1.rs b/solutions/15_traits/traits1.rs new file mode 100644 index 00000000..790873f4 --- /dev/null +++ b/solutions/15_traits/traits1.rs @@ -0,0 +1,32 @@ +// The trait `AppendBar` has only one function which appends "Bar" to any object +// implementing this trait. +trait AppendBar { + fn append_bar(self) -> Self; +} + +impl AppendBar for String { + fn append_bar(self) -> Self { + self + "Bar" + } +} + +fn main() { + let s = String::from("Foo"); + let s = s.append_bar(); + println!("s: {s}"); +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn is_foo_bar() { + assert_eq!(String::from("Foo").append_bar(), "FooBar"); + } + + #[test] + fn is_bar_bar() { + assert_eq!(String::from("").append_bar().append_bar(), "BarBar"); + } +} diff --git a/solutions/15_traits/traits2.rs b/solutions/15_traits/traits2.rs new file mode 100644 index 00000000..0db93e0f --- /dev/null +++ b/solutions/15_traits/traits2.rs @@ -0,0 +1,27 @@ +trait AppendBar { + fn append_bar(self) -> Self; +} + +impl AppendBar for Vec { + fn append_bar(mut self) -> Self { + // ^^^ this is important + self.push(String::from("Bar")); + self + } +} + +fn main() { + // You can optionally experiment here. +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn is_vec_pop_eq_bar() { + let mut foo = vec![String::from("Foo")].append_bar(); + assert_eq!(foo.pop().unwrap(), "Bar"); + assert_eq!(foo.pop().unwrap(), "Foo"); + } +} diff --git a/solutions/15_traits/traits3.rs b/solutions/15_traits/traits3.rs new file mode 100644 index 00000000..3d8ec85e --- /dev/null +++ b/solutions/15_traits/traits3.rs @@ -0,0 +1,36 @@ +trait Licensed { + fn licensing_info(&self) -> String { + "Default license".to_string() + } +} + +struct SomeSoftware { + version_number: i32, +} + +struct OtherSoftware { + version_number: String, +} + +impl Licensed for SomeSoftware {} +impl Licensed for OtherSoftware {} + +fn main() { + // You can optionally experiment here. +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn is_licensing_info_the_same() { + let licensing_info = "Default license"; + let some_software = SomeSoftware { version_number: 1 }; + let other_software = OtherSoftware { + version_number: "v2.0.0".to_string(), + }; + assert_eq!(some_software.licensing_info(), licensing_info); + assert_eq!(other_software.licensing_info(), licensing_info); + } +} diff --git a/solutions/15_traits/traits4.rs b/solutions/15_traits/traits4.rs new file mode 100644 index 00000000..3675b8df --- /dev/null +++ b/solutions/15_traits/traits4.rs @@ -0,0 +1,35 @@ +trait Licensed { + fn licensing_info(&self) -> String { + "Default license".to_string() + } +} + +struct SomeSoftware; +struct OtherSoftware; + +impl Licensed for SomeSoftware {} +impl Licensed for OtherSoftware {} + +fn compare_license_types(software1: impl Licensed, software2: impl Licensed) -> bool { + // ^^^^^^^^^^^^^ ^^^^^^^^^^^^^ + software1.licensing_info() == software2.licensing_info() +} + +fn main() { + // You can optionally experiment here. +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn compare_license_information() { + assert!(compare_license_types(SomeSoftware, OtherSoftware)); + } + + #[test] + fn compare_license_information_backwards() { + assert!(compare_license_types(OtherSoftware, SomeSoftware)); + } +} diff --git a/solutions/15_traits/traits5.rs b/solutions/15_traits/traits5.rs new file mode 100644 index 00000000..1fb426a4 --- /dev/null +++ b/solutions/15_traits/traits5.rs @@ -0,0 +1,39 @@ +trait SomeTrait { + fn some_function(&self) -> bool { + true + } +} + +trait OtherTrait { + fn other_function(&self) -> bool { + true + } +} + +struct SomeStruct; +impl SomeTrait for SomeStruct {} +impl OtherTrait for SomeStruct {} + +struct OtherStruct; +impl SomeTrait for OtherStruct {} +impl OtherTrait for OtherStruct {} + +fn some_func(item: impl SomeTrait + OtherTrait) -> bool { + // ^^^^^^^^^^^^^^^^^^^^^^^^^^^ + item.some_function() && item.other_function() +} + +fn main() { + // You can optionally experiment here. +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_some_func() { + assert!(some_func(SomeStruct)); + assert!(some_func(OtherStruct)); + } +} diff --git a/solutions/16_lifetimes/lifetimes1.rs b/solutions/16_lifetimes/lifetimes1.rs new file mode 100644 index 00000000..ca7b688d --- /dev/null +++ b/solutions/16_lifetimes/lifetimes1.rs @@ -0,0 +1,28 @@ +// The Rust compiler needs to know how to check whether supplied references are +// valid, so that it can let the programmer know if a reference is at risk of +// going out of scope before it is used. Remember, references are borrows and do +// not own their own data. What if their owner goes out of scope? + +fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { + // ^^^^ ^^ ^^ ^^ + if x.len() > y.len() { + x + } else { + y + } +} + +fn main() { + // You can optionally experiment here. +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_longest() { + assert_eq!(longest("abcd", "123"), "abcd"); + assert_eq!(longest("abc", "1234"), "1234"); + } +} diff --git a/solutions/16_lifetimes/lifetimes2.rs b/solutions/16_lifetimes/lifetimes2.rs new file mode 100644 index 00000000..b0f2ef1f --- /dev/null +++ b/solutions/16_lifetimes/lifetimes2.rs @@ -0,0 +1,33 @@ +fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { + if x.len() > y.len() { + x + } else { + y + } +} + +fn main() { + let string1 = String::from("long string is long"); + // Solution1: You can move `strings2` out of the inner block so that it is + // not dropped before the print statement. + let string2 = String::from("xyz"); + let result; + { + result = longest(&string1, &string2); + } + println!("The longest string is '{result}'"); + // `string2` dropped at the end of the function. + + // ========================================================================= + + let string1 = String::from("long string is long"); + let result; + { + let string2 = String::from("xyz"); + result = longest(&string1, &string2); + // Solution2: You can move the print statement into the inner block so + // that it is executed before `string2` is dropped. + println!("The longest string is '{result}'"); + // `string2` dropped here (end of the inner scope). + } +} diff --git a/solutions/16_lifetimes/lifetimes3.rs b/solutions/16_lifetimes/lifetimes3.rs new file mode 100644 index 00000000..16a5a684 --- /dev/null +++ b/solutions/16_lifetimes/lifetimes3.rs @@ -0,0 +1,18 @@ +// Lifetimes are also needed when structs hold references. + +struct Book<'a> { + // ^^^^ added a lifetime annotation + author: &'a str, + // ^^ + title: &'a str, + // ^^ +} + +fn main() { + let book = Book { + author: "George Orwell", + title: "1984", + }; + + println!("{} by {}", book.title, book.author); +} diff --git a/solutions/17_tests/tests1.rs b/solutions/17_tests/tests1.rs new file mode 100644 index 00000000..c52b8b16 --- /dev/null +++ b/solutions/17_tests/tests1.rs @@ -0,0 +1,24 @@ +// Tests are important to ensure that your code does what you think it should +// do. + +fn is_even(n: i64) -> bool { + n % 2 == 0 +} + +fn main() { + // You can optionally experiment here. +} + +#[cfg(test)] +mod tests { + // When writing unit tests, it is common to import everything from the outer + // module (`super`) using a wildcard. + use super::*; + + #[test] + fn you_can_assert() { + assert!(is_even(0)); + assert!(!is_even(-1)); + // ^ You can assert `false` using the negation operator `!`. + } +} diff --git a/solutions/17_tests/tests2.rs b/solutions/17_tests/tests2.rs new file mode 100644 index 00000000..39a0005e --- /dev/null +++ b/solutions/17_tests/tests2.rs @@ -0,0 +1,22 @@ +// Calculates the power of 2 using a bit shift. +// `1 << n` is equivalent to "2 to the power of n". +fn power_of_2(n: u8) -> u64 { + 1 << n +} + +fn main() { + // You can optionally experiment here. +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn you_can_assert_eq() { + assert_eq!(power_of_2(0), 1); + assert_eq!(power_of_2(1), 2); + assert_eq!(power_of_2(2), 4); + assert_eq!(power_of_2(3), 8); + } +} diff --git a/solutions/17_tests/tests3.rs b/solutions/17_tests/tests3.rs new file mode 100644 index 00000000..503f9bc0 --- /dev/null +++ b/solutions/17_tests/tests3.rs @@ -0,0 +1,45 @@ +struct Rectangle { + width: i32, + height: i32, +} + +impl Rectangle { + // Don't change this function. + fn new(width: i32, height: i32) -> Self { + if width <= 0 || height <= 0 { + // Returning a `Result` would be better here. But we want to learn + // how to test functions that can panic. + panic!("Rectangle width and height can't be negative"); + } + + Rectangle { width, height } + } +} + +fn main() { + // You can optionally experiment here. +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn correct_width_and_height() { + let rect = Rectangle::new(10, 20); + assert_eq!(rect.width, 10); // Check width + assert_eq!(rect.height, 20); // Check height + } + + #[test] + #[should_panic] // Added this attribute to check that the test panics. + fn negative_width() { + let _rect = Rectangle::new(-10, 10); + } + + #[test] + #[should_panic] // Added this attribute to check that the test panics. + fn negative_height() { + let _rect = Rectangle::new(10, -10); + } +} diff --git a/solutions/18_iterators/iterators1.rs b/solutions/18_iterators/iterators1.rs new file mode 100644 index 00000000..93a6008a --- /dev/null +++ b/solutions/18_iterators/iterators1.rs @@ -0,0 +1,26 @@ +// When performing operations on elements within a collection, iterators are +// essential. This module helps you get familiar with the structure of using an +// iterator and how to go through elements within an iterable collection. + +fn main() { + // You can optionally experiment here. +} + +#[cfg(test)] +mod tests { + #[test] + fn iterators() { + let my_fav_fruits = ["banana", "custard apple", "avocado", "peach", "raspberry"]; + + // Create an iterator over the array. + let mut fav_fruits_iterator = my_fav_fruits.iter(); + + assert_eq!(fav_fruits_iterator.next(), Some(&"banana")); + assert_eq!(fav_fruits_iterator.next(), Some(&"custard apple")); + assert_eq!(fav_fruits_iterator.next(), Some(&"avocado")); + assert_eq!(fav_fruits_iterator.next(), Some(&"peach")); + assert_eq!(fav_fruits_iterator.next(), Some(&"raspberry")); + assert_eq!(fav_fruits_iterator.next(), None); + // ^^^^ reached the end + } +} diff --git a/solutions/18_iterators/iterators2.rs b/solutions/18_iterators/iterators2.rs new file mode 100644 index 00000000..db05f293 --- /dev/null +++ b/solutions/18_iterators/iterators2.rs @@ -0,0 +1,56 @@ +// In this exercise, you'll learn some of the unique advantages that iterators +// can offer. + +// "hello" -> "Hello" +fn capitalize_first(input: &str) -> String { + let mut chars = input.chars(); + match chars.next() { + None => String::new(), + Some(first) => first.to_uppercase().to_string() + chars.as_str(), + } +} + +// Apply the `capitalize_first` function to a slice of string slices. +// Return a vector of strings. +// ["hello", "world"] -> ["Hello", "World"] +fn capitalize_words_vector(words: &[&str]) -> Vec { + words.iter().map(|word| capitalize_first(word)).collect() +} + +// Apply the `capitalize_first` function again to a slice of string +// slices. Return a single string. +// ["hello", " ", "world"] -> "Hello World" +fn capitalize_words_string(words: &[&str]) -> String { + words.iter().map(|word| capitalize_first(word)).collect() +} + +fn main() { + // You can optionally experiment here. +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_success() { + assert_eq!(capitalize_first("hello"), "Hello"); + } + + #[test] + fn test_empty() { + assert_eq!(capitalize_first(""), ""); + } + + #[test] + fn test_iterate_string_vec() { + let words = vec!["hello", "world"]; + assert_eq!(capitalize_words_vector(&words), ["Hello", "World"]); + } + + #[test] + fn test_iterate_into_string() { + let words = vec!["hello", " ", "world"]; + assert_eq!(capitalize_words_string(&words), "Hello World"); + } +} diff --git a/solutions/18_iterators/iterators3.rs b/solutions/18_iterators/iterators3.rs new file mode 100644 index 00000000..d66d1ef3 --- /dev/null +++ b/solutions/18_iterators/iterators3.rs @@ -0,0 +1,73 @@ +#[derive(Debug, PartialEq, Eq)] +enum DivisionError { + DivideByZero, + NotDivisible, +} + +fn divide(a: i64, b: i64) -> Result { + if b == 0 { + return Err(DivisionError::DivideByZero); + } + + if a % b != 0 { + return Err(DivisionError::NotDivisible); + } + + Ok(a / b) +} + +fn result_with_list() -> Result, DivisionError> { + // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + let numbers = [27, 297, 38502, 81]; + let division_results = numbers.into_iter().map(|n| divide(n, 27)); + // Collects to the expected return type. Returns the first error in the + // division results (if one exists). + division_results.collect() +} + +fn list_of_results() -> Vec> { + // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + let numbers = [27, 297, 38502, 81]; + let division_results = numbers.into_iter().map(|n| divide(n, 27)); + // Collects to the expected return type. + division_results.collect() +} + +fn main() { + // You can optionally experiment here. +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_success() { + assert_eq!(divide(81, 9), Ok(9)); + } + + #[test] + fn test_divide_by_0() { + assert_eq!(divide(81, 0), Err(DivisionError::DivideByZero)); + } + + #[test] + fn test_not_divisible() { + assert_eq!(divide(81, 6), Err(DivisionError::NotDivisible)); + } + + #[test] + fn test_divide_0_by_something() { + assert_eq!(divide(0, 81), Ok(0)); + } + + #[test] + fn test_result_with_list() { + assert_eq!(result_with_list().unwrap(), [1, 11, 1426, 3]); + } + + #[test] + fn test_list_of_results() { + assert_eq!(list_of_results(), [Ok(1), Ok(11), Ok(1426), Ok(3)]); + } +} diff --git a/solutions/18_iterators/iterators4.rs b/solutions/18_iterators/iterators4.rs new file mode 100644 index 00000000..4c3c49d9 --- /dev/null +++ b/solutions/18_iterators/iterators4.rs @@ -0,0 +1,71 @@ +// 3 possible solutions are presented. + +// With `for` loop and a mutable variable. +fn factorial_for(num: u64) -> u64 { + let mut result = 1; + + for x in 2..=num { + result *= x; + } + + result +} + +// Equivalent to `factorial_for` but shorter and without a `for` loop and +// mutable variables. +fn factorial_fold(num: u64) -> u64 { + // Case num==0: The iterator 2..=0 is empty + // -> The initial value of `fold` is returned which is 1. + // Case num==1: The iterator 2..=1 is also empty + // -> The initial value 1 is returned. + // Case num==2: The iterator 2..=2 contains one element + // -> The initial value 1 is multiplied by 2 and the result + // is returned. + // Case num==3: The iterator 2..=3 contains 2 elements + // -> 1 * 2 is calculated, then the result 2 is multiplied by + // the second element 3 so the result 6 is returned. + // And so on… + (2..=num).fold(1, |acc, x| acc * x) +} + +// Equivalent to `factorial_fold` but with a built-in method that is suggested +// by Clippy. +fn factorial_product(num: u64) -> u64 { + (2..=num).product() +} + +fn main() { + // You can optionally experiment here. +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn factorial_of_0() { + assert_eq!(factorial_for(0), 1); + assert_eq!(factorial_fold(0), 1); + assert_eq!(factorial_product(0), 1); + } + + #[test] + fn factorial_of_1() { + assert_eq!(factorial_for(1), 1); + assert_eq!(factorial_fold(1), 1); + assert_eq!(factorial_product(1), 1); + } + #[test] + fn factorial_of_2() { + assert_eq!(factorial_for(2), 2); + assert_eq!(factorial_fold(2), 2); + assert_eq!(factorial_product(2), 2); + } + + #[test] + fn factorial_of_4() { + assert_eq!(factorial_for(4), 24); + assert_eq!(factorial_fold(4), 24); + assert_eq!(factorial_product(4), 24); + } +} diff --git a/solutions/18_iterators/iterators5.rs b/solutions/18_iterators/iterators5.rs new file mode 100644 index 00000000..402c81b0 --- /dev/null +++ b/solutions/18_iterators/iterators5.rs @@ -0,0 +1,150 @@ +// Let's define a simple model to track Rustlings' exercise progress. Progress +// will be modelled using a hash map. The name of the exercise is the key and +// the progress is the value. Two counting functions were created to count the +// number of exercises with a given progress. Recreate this counting +// functionality using iterators. Try to not use imperative loops (for/while). + +use std::collections::HashMap; + +#[derive(Clone, Copy, PartialEq, Eq)] +enum Progress { + None, + Some, + Complete, +} + +fn count_for(map: &HashMap, value: Progress) -> usize { + let mut count = 0; + for val in map.values() { + if *val == value { + count += 1; + } + } + count +} + +fn count_iterator(map: &HashMap, value: Progress) -> usize { + // `map` is a hash map with `String` keys and `Progress` values. + // map = { "variables1": Complete, "from_str": None, … } + map.values().filter(|val| **val == value).count() +} + +fn count_collection_for(collection: &[HashMap], value: Progress) -> usize { + let mut count = 0; + for map in collection { + count += count_for(map, value); + } + count +} + +fn count_collection_iterator(collection: &[HashMap], value: Progress) -> usize { + // `collection` is a slice of hash maps. + // collection = [{ "variables1": Complete, "from_str": None, … }, + // { "variables2": Complete, … }, … ] + collection + .iter() + .map(|map| count_iterator(map, value)) + .sum() +} + +fn main() { + // You can optionally experiment here. +} + +#[cfg(test)] +mod tests { + use super::*; + + fn get_map() -> HashMap { + use Progress::*; + + let mut map = HashMap::new(); + map.insert(String::from("variables1"), Complete); + map.insert(String::from("functions1"), Complete); + map.insert(String::from("hashmap1"), Complete); + map.insert(String::from("arc1"), Some); + map.insert(String::from("as_ref_mut"), None); + map.insert(String::from("from_str"), None); + + map + } + + fn get_vec_map() -> Vec> { + use Progress::*; + + let map = get_map(); + + let mut other = HashMap::new(); + other.insert(String::from("variables2"), Complete); + other.insert(String::from("functions2"), Complete); + other.insert(String::from("if1"), Complete); + other.insert(String::from("from_into"), None); + other.insert(String::from("try_from_into"), None); + + vec![map, other] + } + + #[test] + fn count_complete() { + let map = get_map(); + assert_eq!(count_iterator(&map, Progress::Complete), 3); + } + + #[test] + fn count_some() { + let map = get_map(); + assert_eq!(count_iterator(&map, Progress::Some), 1); + } + + #[test] + fn count_none() { + let map = get_map(); + assert_eq!(count_iterator(&map, Progress::None), 2); + } + + #[test] + fn count_complete_equals_for() { + let map = get_map(); + let progress_states = [Progress::Complete, Progress::Some, Progress::None]; + for progress_state in progress_states { + assert_eq!( + count_for(&map, progress_state), + count_iterator(&map, progress_state), + ); + } + } + + #[test] + fn count_collection_complete() { + let collection = get_vec_map(); + assert_eq!( + count_collection_iterator(&collection, Progress::Complete), + 6, + ); + } + + #[test] + fn count_collection_some() { + let collection = get_vec_map(); + assert_eq!(count_collection_iterator(&collection, Progress::Some), 1); + } + + #[test] + fn count_collection_none() { + let collection = get_vec_map(); + assert_eq!(count_collection_iterator(&collection, Progress::None), 4); + } + + #[test] + fn count_collection_equals_for() { + let collection = get_vec_map(); + let progress_states = [Progress::Complete, Progress::Some, Progress::None]; + + for progress_state in progress_states { + assert_eq!( + count_collection_for(&collection, progress_state), + count_collection_iterator(&collection, progress_state), + ); + } + } +} diff --git a/solutions/19_smart_pointers/arc1.rs b/solutions/19_smart_pointers/arc1.rs new file mode 100644 index 00000000..a520dfe6 --- /dev/null +++ b/solutions/19_smart_pointers/arc1.rs @@ -0,0 +1,42 @@ +// In this exercise, we are given a `Vec` of u32 called `numbers` with values +// ranging from 0 to 99. We would like to use this set of numbers within 8 +// different threads simultaneously. Each thread is going to get the sum of +// every eighth value with an offset. +// +// The first thread (offset 0), will sum 0, 8, 16, … +// The second thread (offset 1), will sum 1, 9, 17, … +// The third thread (offset 2), will sum 2, 10, 18, … +// … +// The eighth thread (offset 7), will sum 7, 15, 23, … +// +// Because we are using threads, our values need to be thread-safe. Therefore, +// we are using `Arc`. + +// Don't change the lines below. +#![forbid(unused_imports)] +use std::{sync::Arc, thread}; + +fn main() { + let numbers: Vec<_> = (0..100u32).collect(); + + let shared_numbers = Arc::new(numbers); + // ^^^^^^^^^^^^^^^^^ + + let mut join_handles = Vec::new(); + + for offset in 0..8 { + let child_numbers = Arc::clone(&shared_numbers); + // ^^^^^^^^^^^^^^^^^^^^^^^^^^^ + + let handle = thread::spawn(move || { + let sum: u32 = child_numbers.iter().filter(|&&n| n % 8 == offset).sum(); + println!("Sum of offset {offset} is {sum}"); + }); + + join_handles.push(handle); + } + + for handle in join_handles.into_iter() { + handle.join().unwrap(); + } +} diff --git a/solutions/19_smart_pointers/box1.rs b/solutions/19_smart_pointers/box1.rs new file mode 100644 index 00000000..189cc562 --- /dev/null +++ b/solutions/19_smart_pointers/box1.rs @@ -0,0 +1,47 @@ +// At compile time, Rust needs to know how much space a type takes up. This +// becomes problematic for recursive types, where a value can have as part of +// itself another value of the same type. To get around the issue, we can use a +// `Box` - a smart pointer used to store data on the heap, which also allows us +// to wrap a recursive type. +// +// The recursive type we're implementing in this exercise is the "cons list", a +// data structure frequently found in functional programming languages. Each +// item in a cons list contains two elements: The value of the current item and +// the next item. The last item is a value called `Nil`. + +#[derive(PartialEq, Debug)] +enum List { + Cons(i32, Box), + Nil, +} + +fn create_empty_list() -> List { + List::Nil +} + +fn create_non_empty_list() -> List { + List::Cons(42, Box::new(List::Nil)) +} + +fn main() { + println!("This is an empty cons list: {:?}", create_empty_list()); + println!( + "This is a non-empty cons list: {:?}", + create_non_empty_list(), + ); +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_create_empty_list() { + assert_eq!(create_empty_list(), List::Nil); + } + + #[test] + fn test_create_non_empty_list() { + assert_ne!(create_empty_list(), create_non_empty_list()); + } +} diff --git a/solutions/19_smart_pointers/cow1.rs b/solutions/19_smart_pointers/cow1.rs new file mode 100644 index 00000000..0a21a91b --- /dev/null +++ b/solutions/19_smart_pointers/cow1.rs @@ -0,0 +1,68 @@ +// This exercise explores the `Cow` (Clone-On-Write) smart pointer. It can +// enclose and provide immutable access to borrowed data and clone the data +// lazily when mutation or ownership is required. The type is designed to work +// with general borrowed data via the `Borrow` trait. + +use std::borrow::Cow; + +fn abs_all(input: &mut Cow<[i32]>) { + for ind in 0..input.len() { + let value = input[ind]; + if value < 0 { + // Clones into a vector if not already owned. + input.to_mut()[ind] = -value; + } + } +} + +fn main() { + // You can optionally experiment here. +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn reference_mutation() { + // Clone occurs because `input` needs to be mutated. + let vec = vec![-1, 0, 1]; + let mut input = Cow::from(&vec); + abs_all(&mut input); + assert!(matches!(input, Cow::Owned(_))); + } + + #[test] + fn reference_no_mutation() { + // No clone occurs because `input` doesn't need to be mutated. + let vec = vec![0, 1, 2]; + let mut input = Cow::from(&vec); + abs_all(&mut input); + assert!(matches!(input, Cow::Borrowed(_))); + // ^^^^^^^^^^^^^^^^ + } + + #[test] + fn owned_no_mutation() { + // We can also pass `vec` without `&` so `Cow` owns it directly. In this + // case, no mutation occurs and thus also no clone. But the result is + // still owned because it was never borrowed or mutated. + let vec = vec![0, 1, 2]; + let mut input = Cow::from(vec); + abs_all(&mut input); + assert!(matches!(input, Cow::Owned(_))); + // ^^^^^^^^^^^^^ + } + + #[test] + fn owned_mutation() { + // Of course this is also the case if a mutation does occur. In this + // case, the call to `to_mut()` in the `abs_all` function returns a + // reference to the same data as before. + let vec = vec![-1, 0, 1]; + let mut input = Cow::from(vec); + abs_all(&mut input); + assert!(matches!(input, Cow::Owned(_))); + // ^^^^^^^^^^^^^ + } +} diff --git a/solutions/19_smart_pointers/rc1.rs b/solutions/19_smart_pointers/rc1.rs new file mode 100644 index 00000000..c0a41abf --- /dev/null +++ b/solutions/19_smart_pointers/rc1.rs @@ -0,0 +1,104 @@ +// In this exercise, we want to express the concept of multiple owners via the +// `Rc` type. This is a model of our solar system - there is a `Sun` type and +// multiple `Planet`s. The planets take ownership of the sun, indicating that +// they revolve around the sun. + +use std::rc::Rc; + +#[derive(Debug)] +struct Sun; + +#[derive(Debug)] +enum Planet { + Mercury(Rc), + Venus(Rc), + Earth(Rc), + Mars(Rc), + Jupiter(Rc), + Saturn(Rc), + Uranus(Rc), + Neptune(Rc), +} + +impl Planet { + fn details(&self) { + println!("Hi from {self:?}!"); + } +} + +fn main() { + // You can optionally experiment here. +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn rc1() { + let sun = Rc::new(Sun); + println!("reference count = {}", Rc::strong_count(&sun)); // 1 reference + + let mercury = Planet::Mercury(Rc::clone(&sun)); + println!("reference count = {}", Rc::strong_count(&sun)); // 2 references + mercury.details(); + + let venus = Planet::Venus(Rc::clone(&sun)); + println!("reference count = {}", Rc::strong_count(&sun)); // 3 references + venus.details(); + + let earth = Planet::Earth(Rc::clone(&sun)); + println!("reference count = {}", Rc::strong_count(&sun)); // 4 references + earth.details(); + + let mars = Planet::Mars(Rc::clone(&sun)); + println!("reference count = {}", Rc::strong_count(&sun)); // 5 references + mars.details(); + + let jupiter = Planet::Jupiter(Rc::clone(&sun)); + println!("reference count = {}", Rc::strong_count(&sun)); // 6 references + jupiter.details(); + + let saturn = Planet::Saturn(Rc::clone(&sun)); + println!("reference count = {}", Rc::strong_count(&sun)); // 7 references + saturn.details(); + + // TODO + let uranus = Planet::Uranus(Rc::clone(&sun)); + println!("reference count = {}", Rc::strong_count(&sun)); // 8 references + uranus.details(); + + // TODO + let neptune = Planet::Neptune(Rc::clone(&sun)); + println!("reference count = {}", Rc::strong_count(&sun)); // 9 references + neptune.details(); + + assert_eq!(Rc::strong_count(&sun), 9); + + drop(neptune); + println!("reference count = {}", Rc::strong_count(&sun)); // 8 references + + drop(uranus); + println!("reference count = {}", Rc::strong_count(&sun)); // 7 references + + drop(saturn); + println!("reference count = {}", Rc::strong_count(&sun)); // 6 references + + drop(jupiter); + println!("reference count = {}", Rc::strong_count(&sun)); // 5 references + + drop(mars); + println!("reference count = {}", Rc::strong_count(&sun)); // 4 references + + drop(earth); + println!("reference count = {}", Rc::strong_count(&sun)); // 3 references + + drop(venus); + println!("reference count = {}", Rc::strong_count(&sun)); // 2 references + + drop(mercury); + println!("reference count = {}", Rc::strong_count(&sun)); // 1 reference + + assert_eq!(Rc::strong_count(&sun), 1); + } +} diff --git a/solutions/20_threads/threads1.rs b/solutions/20_threads/threads1.rs new file mode 100644 index 00000000..7f3dd29a --- /dev/null +++ b/solutions/20_threads/threads1.rs @@ -0,0 +1,37 @@ +// This program spawns multiple threads that each run for at least 250ms, and +// each thread returns how much time they took to complete. The program should +// wait until all the spawned threads have finished and should collect their +// return values into a vector. + +use std::{ + thread, + time::{Duration, Instant}, +}; + +fn main() { + let mut handles = Vec::new(); + for i in 0..10 { + let handle = thread::spawn(move || { + let start = Instant::now(); + thread::sleep(Duration::from_millis(250)); + println!("Thread {i} done"); + start.elapsed().as_millis() + }); + handles.push(handle); + } + + let mut results = Vec::new(); + for handle in handles { + // Collect the results of all threads into the `results` vector. + results.push(handle.join().unwrap()); + } + + if results.len() != 10 { + panic!("Oh no! Some thread isn't done yet!"); + } + + println!(); + for (i, result) in results.into_iter().enumerate() { + println!("Thread {i} took {result}ms"); + } +} diff --git a/solutions/20_threads/threads2.rs b/solutions/20_threads/threads2.rs new file mode 100644 index 00000000..bc268d63 --- /dev/null +++ b/solutions/20_threads/threads2.rs @@ -0,0 +1,41 @@ +// Building on the last exercise, we want all of the threads to complete their +// work. But this time, the spawned threads need to be in charge of updating a +// shared value: `JobStatus.jobs_done` + +use std::{ + sync::{Arc, Mutex}, + thread, + time::Duration, +}; + +struct JobStatus { + jobs_done: u32, +} + +fn main() { + // `Arc` isn't enough if you want a **mutable** shared state. + // We need to wrap the value with a `Mutex`. + let status = Arc::new(Mutex::new(JobStatus { jobs_done: 0 })); + // ^^^^^^^^^^^ ^ + + let mut handles = Vec::new(); + for _ in 0..10 { + let status_shared = Arc::clone(&status); + let handle = thread::spawn(move || { + thread::sleep(Duration::from_millis(250)); + + // Lock before you update a shared value. + status_shared.lock().unwrap().jobs_done += 1; + // ^^^^^^^^^^^^^^^^ + }); + handles.push(handle); + } + + // Waiting for all jobs to complete. + for handle in handles { + handle.join().unwrap(); + } + + println!("Jobs done: {}", status.lock().unwrap().jobs_done); + // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +} diff --git a/solutions/20_threads/threads3.rs b/solutions/20_threads/threads3.rs new file mode 100644 index 00000000..cd2dfbe3 --- /dev/null +++ b/solutions/20_threads/threads3.rs @@ -0,0 +1,66 @@ +use std::{sync::mpsc, thread, time::Duration}; + +struct Queue { + length: u32, + first_half: Vec, + second_half: Vec, +} + +impl Queue { + fn new() -> Self { + Self { + length: 10, + first_half: vec![1, 2, 3, 4, 5], + second_half: vec![6, 7, 8, 9, 10], + } + } +} + +fn send_tx(q: Queue, tx: mpsc::Sender) { + // Clone the sender `tx` first. + let tx_clone = tx.clone(); + thread::spawn(move || { + for val in q.first_half { + println!("Sending {val:?}"); + // Then use the clone in the first thread. This means that + // `tx_clone` is moved to the first thread and `tx` to the second. + tx_clone.send(val).unwrap(); + thread::sleep(Duration::from_millis(250)); + } + }); + + thread::spawn(move || { + for val in q.second_half { + println!("Sending {val:?}"); + tx.send(val).unwrap(); + thread::sleep(Duration::from_millis(250)); + } + }); +} + +fn main() { + // You can optionally experiment here. +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn threads3() { + let (tx, rx) = mpsc::channel(); + let queue = Queue::new(); + let queue_length = queue.length; + + send_tx(queue, tx); + + let mut total_received: u32 = 0; + for received in rx { + println!("Got: {received}"); + total_received += 1; + } + + println!("Number of received values: {total_received}"); + assert_eq!(total_received, queue_length); + } +} diff --git a/solutions/21_macros/macros1.rs b/solutions/21_macros/macros1.rs new file mode 100644 index 00000000..1b861564 --- /dev/null +++ b/solutions/21_macros/macros1.rs @@ -0,0 +1,10 @@ +macro_rules! my_macro { + () => { + println!("Check out my macro!"); + }; +} + +fn main() { + my_macro!(); + // ^ +} diff --git a/solutions/21_macros/macros2.rs b/solutions/21_macros/macros2.rs new file mode 100644 index 00000000..b6fd5d2c --- /dev/null +++ b/solutions/21_macros/macros2.rs @@ -0,0 +1,10 @@ +// Moved the macro definition to be before its call. +macro_rules! my_macro { + () => { + println!("Check out my macro!"); + }; +} + +fn main() { + my_macro!(); +} diff --git a/solutions/21_macros/macros3.rs b/solutions/21_macros/macros3.rs new file mode 100644 index 00000000..df35be4d --- /dev/null +++ b/solutions/21_macros/macros3.rs @@ -0,0 +1,13 @@ +// Added the attribute `macro_use` attribute. +#[macro_use] +mod macros { + macro_rules! my_macro { + () => { + println!("Check out my macro!"); + }; + } +} + +fn main() { + my_macro!(); +} diff --git a/solutions/21_macros/macros4.rs b/solutions/21_macros/macros4.rs new file mode 100644 index 00000000..41bcad16 --- /dev/null +++ b/solutions/21_macros/macros4.rs @@ -0,0 +1,15 @@ +// Added semicolons to separate the macro arms. +#[rustfmt::skip] +macro_rules! my_macro { + () => { + println!("Check out my macro!"); + }; + ($val:expr) => { + println!("Look at this other macro: {}", $val); + }; +} + +fn main() { + my_macro!(); + my_macro!(7777); +} diff --git a/solutions/22_clippy/clippy1.rs b/solutions/22_clippy/clippy1.rs new file mode 100644 index 00000000..b9d1ec17 --- /dev/null +++ b/solutions/22_clippy/clippy1.rs @@ -0,0 +1,17 @@ +// The Clippy tool is a collection of lints to analyze your code so you can +// catch common mistakes and improve your Rust code. +// +// For these exercises, the code will fail to compile when there are Clippy +// warnings. Check Clippy's suggestions from the output to solve the exercise. + +use std::f32::consts::PI; + +fn main() { + // Use the more accurate `PI` constant. + let pi = PI; + let radius: f32 = 5.0; + + let area = pi * radius.powi(2); + + println!("The area of a circle with radius {radius:.2} is {area:.5}"); +} diff --git a/solutions/22_clippy/clippy2.rs b/solutions/22_clippy/clippy2.rs new file mode 100644 index 00000000..7f635628 --- /dev/null +++ b/solutions/22_clippy/clippy2.rs @@ -0,0 +1,10 @@ +fn main() { + let mut res = 42; + let option = Some(12); + // Use `if-let` instead of iteration. + if let Some(x) = option { + res += x; + } + + println!("{res}"); +} diff --git a/solutions/22_clippy/clippy3.rs b/solutions/22_clippy/clippy3.rs new file mode 100644 index 00000000..811d1847 --- /dev/null +++ b/solutions/22_clippy/clippy3.rs @@ -0,0 +1,31 @@ +use std::mem; + +#[rustfmt::skip] +#[allow(unused_variables, unused_assignments)] +fn main() { + let my_option: Option<()> = None; + // `unwrap` of an `Option` after checking if it is `None` will panic. + // Use `if-let` instead. + if let Some(value) = my_option { + println!("{value:?}"); + } + + // A comma was missing. + let my_arr = &[ + -1, -2, -3, + -4, -5, -6, + ]; + println!("My array! Here it is: {:?}", my_arr); + + let mut my_empty_vec = vec![1, 2, 3, 4, 5]; + // `resize` mutates a vector instead of returning a new one. + // `resize(0, …)` clears a vector, so it is better to use `clear`. + my_empty_vec.clear(); + println!("This Vec is empty, see? {my_empty_vec:?}"); + + let mut value_a = 45; + let mut value_b = 66; + // Use `mem::swap` to correctly swap two values. + mem::swap(&mut value_a, &mut value_b); + println!("value a: {}; value b: {}", value_a, value_b); +} diff --git a/solutions/23_conversions/as_ref_mut.rs b/solutions/23_conversions/as_ref_mut.rs new file mode 100644 index 00000000..91b12bac --- /dev/null +++ b/solutions/23_conversions/as_ref_mut.rs @@ -0,0 +1,59 @@ +// AsRef and AsMut allow for cheap reference-to-reference conversions. Read more +// about them at https://doc.rust-lang.org/std/convert/trait.AsRef.html and +// https://doc.rust-lang.org/std/convert/trait.AsMut.html, respectively. + +// Obtain the number of bytes (not characters) in the given argument. +fn byte_counter>(arg: T) -> usize { + arg.as_ref().as_bytes().len() +} + +// Obtain the number of characters (not bytes) in the given argument. +fn char_counter>(arg: T) -> usize { + arg.as_ref().chars().count() +} + +// Squares a number using `as_mut()`. +fn num_sq>(arg: &mut T) { + let arg = arg.as_mut(); + *arg = *arg * *arg; +} + +fn main() { + // You can optionally experiment here. +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn different_counts() { + let s = "Café au lait"; + assert_ne!(char_counter(s), byte_counter(s)); + } + + #[test] + fn same_counts() { + let s = "Cafe au lait"; + assert_eq!(char_counter(s), byte_counter(s)); + } + + #[test] + fn different_counts_using_string() { + let s = String::from("Café au lait"); + assert_ne!(char_counter(s.clone()), byte_counter(s)); + } + + #[test] + fn same_counts_using_string() { + let s = String::from("Cafe au lait"); + assert_eq!(char_counter(s.clone()), byte_counter(s)); + } + + #[test] + fn mut_box() { + let mut num: Box = Box::new(3); + num_sq(&mut num); + assert_eq!(*num, 9); + } +} diff --git a/solutions/23_conversions/from_into.rs b/solutions/23_conversions/from_into.rs new file mode 100644 index 00000000..cec23cb4 --- /dev/null +++ b/solutions/23_conversions/from_into.rs @@ -0,0 +1,136 @@ +// The `From` trait is used for value-to-value conversions. If `From` is +// implemented, an implementation of `Into` is automatically provided. +// You can read more about it in the documentation: +// https://doc.rust-lang.org/std/convert/trait.From.html + +#[derive(Debug)] +struct Person { + name: String, + age: u8, +} + +// We implement the Default trait to use it as a fallback when the provided +// string is not convertible into a `Person` object. +impl Default for Person { + fn default() -> Self { + Self { + name: String::from("John"), + age: 30, + } + } +} + +impl From<&str> for Person { + fn from(s: &str) -> Self { + let mut split = s.split(','); + let (Some(name), Some(age), None) = (split.next(), split.next(), split.next()) else { + // ^^^^ there should be no third element + return Self::default(); + }; + + if name.is_empty() { + return Self::default(); + } + + let Ok(age) = age.parse() else { + return Self::default(); + }; + + Self { + name: name.into(), + age, + } + } +} + +fn main() { + // Use the `from` function. + let p1 = Person::from("Mark,20"); + println!("{p1:?}"); + + // Since `From` is implemented for Person, we are able to use `Into`. + let p2: Person = "Gerald,70".into(); + println!("{p2:?}"); +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_default() { + let dp = Person::default(); + assert_eq!(dp.name, "John"); + assert_eq!(dp.age, 30); + } + + #[test] + fn test_bad_convert() { + let p = Person::from(""); + assert_eq!(p.name, "John"); + assert_eq!(p.age, 30); + } + + #[test] + fn test_good_convert() { + let p = Person::from("Mark,20"); + assert_eq!(p.name, "Mark"); + assert_eq!(p.age, 20); + } + + #[test] + fn test_bad_age() { + let p = Person::from("Mark,twenty"); + assert_eq!(p.name, "John"); + assert_eq!(p.age, 30); + } + + #[test] + fn test_missing_comma_and_age() { + let p: Person = Person::from("Mark"); + assert_eq!(p.name, "John"); + assert_eq!(p.age, 30); + } + + #[test] + fn test_missing_age() { + let p: Person = Person::from("Mark,"); + assert_eq!(p.name, "John"); + assert_eq!(p.age, 30); + } + + #[test] + fn test_missing_name() { + let p: Person = Person::from(",1"); + assert_eq!(p.name, "John"); + assert_eq!(p.age, 30); + } + + #[test] + fn test_missing_name_and_age() { + let p: Person = Person::from(","); + assert_eq!(p.name, "John"); + assert_eq!(p.age, 30); + } + + #[test] + fn test_missing_name_and_invalid_age() { + let p: Person = Person::from(",one"); + assert_eq!(p.name, "John"); + assert_eq!(p.age, 30); + } + + #[test] + fn test_trailing_comma() { + let p: Person = Person::from("Mike,32,"); + assert_eq!(p.name, "John"); + assert_eq!(p.age, 30); + } + + #[test] + fn test_trailing_comma_and_some_string() { + let p: Person = Person::from("Mike,32,dog"); + assert_eq!(p.name, "John"); + assert_eq!(p.age, 30); + } +} diff --git a/solutions/23_conversions/from_str.rs b/solutions/23_conversions/from_str.rs new file mode 100644 index 00000000..005b5012 --- /dev/null +++ b/solutions/23_conversions/from_str.rs @@ -0,0 +1,117 @@ +// This is similar to the previous `from_into` exercise. But this time, we'll +// implement `FromStr` and return errors instead of falling back to a default +// value. Additionally, upon implementing `FromStr`, you can use the `parse` +// method on strings to generate an object of the implementor type. You can read +// more about it in the documentation: +// https://doc.rust-lang.org/std/str/trait.FromStr.html + +use std::num::ParseIntError; +use std::str::FromStr; + +#[derive(Debug, PartialEq)] +struct Person { + name: String, + age: u8, +} + +// We will use this error type for the `FromStr` implementation. +#[derive(Debug, PartialEq)] +enum ParsePersonError { + // Incorrect number of fields + BadLen, + // Empty name field + NoName, + // Wrapped error from parse::() + ParseInt(ParseIntError), +} + +impl FromStr for Person { + type Err = ParsePersonError; + + fn from_str(s: &str) -> Result { + let mut split = s.split(','); + let (Some(name), Some(age), None) = (split.next(), split.next(), split.next()) else { + // ^^^^ there should be no third element + return Err(ParsePersonError::BadLen); + }; + + if name.is_empty() { + return Err(ParsePersonError::NoName); + } + + let age = age.parse().map_err(ParsePersonError::ParseInt)?; + + Ok(Self { + name: name.into(), + age, + }) + } +} + +fn main() { + let p = "Mark,20".parse::(); + println!("{p:?}"); +} + +#[cfg(test)] +mod tests { + use super::*; + use ParsePersonError::*; + + #[test] + fn empty_input() { + assert_eq!("".parse::(), Err(BadLen)); + } + + #[test] + fn good_input() { + let p = "John,32".parse::(); + assert!(p.is_ok()); + let p = p.unwrap(); + assert_eq!(p.name, "John"); + assert_eq!(p.age, 32); + } + + #[test] + fn missing_age() { + assert!(matches!("John,".parse::(), Err(ParseInt(_)))); + } + + #[test] + fn invalid_age() { + assert!(matches!("John,twenty".parse::(), Err(ParseInt(_)))); + } + + #[test] + fn missing_comma_and_age() { + assert_eq!("John".parse::(), Err(BadLen)); + } + + #[test] + fn missing_name() { + assert_eq!(",1".parse::(), Err(NoName)); + } + + #[test] + fn missing_name_and_age() { + assert!(matches!(",".parse::(), Err(NoName | ParseInt(_)))); + } + + #[test] + fn missing_name_and_invalid_age() { + assert!(matches!( + ",one".parse::(), + Err(NoName | ParseInt(_)), + )); + } + + #[test] + fn trailing_comma() { + assert_eq!("John,32,".parse::(), Err(BadLen)); + } + + #[test] + fn trailing_comma_and_some_string() { + assert_eq!("John,32,man".parse::(), Err(BadLen)); + } +} diff --git a/solutions/23_conversions/try_from_into.rs b/solutions/23_conversions/try_from_into.rs new file mode 100644 index 00000000..acb7721d --- /dev/null +++ b/solutions/23_conversions/try_from_into.rs @@ -0,0 +1,192 @@ +// `TryFrom` is a simple and safe type conversion that may fail in a controlled +// way under some circumstances. Basically, this is the same as `From`. The main +// difference is that this should return a `Result` type instead of the target +// type itself. You can read more about it in the documentation: +// https://doc.rust-lang.org/std/convert/trait.TryFrom.html + +use std::convert::{TryFrom, TryInto}; + +#[derive(Debug, PartialEq)] +struct Color { + red: u8, + green: u8, + blue: u8, +} + +// We will use this error type for the `TryFrom` conversions. +#[derive(Debug, PartialEq)] +enum IntoColorError { + // Incorrect length of slice + BadLen, + // Integer conversion error + IntConversion, +} + +impl TryFrom<(i16, i16, i16)> for Color { + type Error = IntoColorError; + + fn try_from(tuple: (i16, i16, i16)) -> Result { + let (Ok(red), Ok(green), Ok(blue)) = ( + u8::try_from(tuple.0), + u8::try_from(tuple.1), + u8::try_from(tuple.2), + ) else { + return Err(IntoColorError::IntConversion); + }; + + Ok(Self { red, green, blue }) + } +} + +impl TryFrom<[i16; 3]> for Color { + type Error = IntoColorError; + + fn try_from(arr: [i16; 3]) -> Result { + // Reuse the implementation for a tuple. + Self::try_from((arr[0], arr[1], arr[2])) + } +} + +impl TryFrom<&[i16]> for Color { + type Error = IntoColorError; + + fn try_from(slice: &[i16]) -> Result { + // Check the length. + if slice.len() != 3 { + return Err(IntoColorError::BadLen); + } + + // Reuse the implementation for a tuple. + Self::try_from((slice[0], slice[1], slice[2])) + } +} + +fn main() { + // Using the `try_from` function. + let c1 = Color::try_from((183, 65, 14)); + println!("{c1:?}"); + + // Since `TryFrom` is implemented for `Color`, we can use `TryInto`. + let c2: Result = [183, 65, 14].try_into(); + println!("{c2:?}"); + + let v = vec![183, 65, 14]; + // With slice we should use the `try_from` function + let c3 = Color::try_from(&v[..]); + println!("{c3:?}"); + // or put the slice within round brackets and use `try_into`. + let c4: Result = (&v[..]).try_into(); + println!("{c4:?}"); +} + +#[cfg(test)] +mod tests { + use super::*; + use IntoColorError::*; + + #[test] + fn test_tuple_out_of_range_positive() { + assert_eq!(Color::try_from((256, 1000, 10000)), Err(IntConversion)); + } + + #[test] + fn test_tuple_out_of_range_negative() { + assert_eq!(Color::try_from((-1, -10, -256)), Err(IntConversion)); + } + + #[test] + fn test_tuple_sum() { + assert_eq!(Color::try_from((-1, 255, 255)), Err(IntConversion)); + } + + #[test] + fn test_tuple_correct() { + let c: Result = (183, 65, 14).try_into(); + assert!(c.is_ok()); + assert_eq!( + c.unwrap(), + Color { + red: 183, + green: 65, + blue: 14, + } + ); + } + + #[test] + fn test_array_out_of_range_positive() { + let c: Result = [1000, 10000, 256].try_into(); + assert_eq!(c, Err(IntConversion)); + } + + #[test] + fn test_array_out_of_range_negative() { + let c: Result = [-10, -256, -1].try_into(); + assert_eq!(c, Err(IntConversion)); + } + + #[test] + fn test_array_sum() { + let c: Result = [-1, 255, 255].try_into(); + assert_eq!(c, Err(IntConversion)); + } + + #[test] + fn test_array_correct() { + let c: Result = [183, 65, 14].try_into(); + assert!(c.is_ok()); + assert_eq!( + c.unwrap(), + Color { + red: 183, + green: 65, + blue: 14 + } + ); + } + + #[test] + fn test_slice_out_of_range_positive() { + let arr = [10000, 256, 1000]; + assert_eq!(Color::try_from(&arr[..]), Err(IntConversion)); + } + + #[test] + fn test_slice_out_of_range_negative() { + let arr = [-256, -1, -10]; + assert_eq!(Color::try_from(&arr[..]), Err(IntConversion)); + } + + #[test] + fn test_slice_sum() { + let arr = [-1, 255, 255]; + assert_eq!(Color::try_from(&arr[..]), Err(IntConversion)); + } + + #[test] + fn test_slice_correct() { + let v = vec![183, 65, 14]; + let c: Result = Color::try_from(&v[..]); + assert!(c.is_ok()); + assert_eq!( + c.unwrap(), + Color { + red: 183, + green: 65, + blue: 14, + } + ); + } + + #[test] + fn test_slice_excess_length() { + let v = vec![0, 0, 0, 0]; + assert_eq!(Color::try_from(&v[..]), Err(BadLen)); + } + + #[test] + fn test_slice_insufficient_length() { + let v = vec![0, 0]; + assert_eq!(Color::try_from(&v[..]), Err(BadLen)); + } +} diff --git a/solutions/23_conversions/using_as.rs b/solutions/23_conversions/using_as.rs new file mode 100644 index 00000000..14b92ebf --- /dev/null +++ b/solutions/23_conversions/using_as.rs @@ -0,0 +1,24 @@ +// Type casting in Rust is done via the usage of the `as` operator. +// Note that the `as` operator is not only used when type casting. It also helps +// with renaming imports. + +fn average(values: &[f64]) -> f64 { + let total = values.iter().sum::(); + total / values.len() as f64 + // ^^^^^^ +} + +fn main() { + let values = [3.5, 0.3, 13.0, 11.7]; + println!("{}", average(&values)); +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn returns_proper_type_and_value() { + assert_eq!(average(&[3.5, 0.3, 13.0, 11.7]), 7.125); + } +} diff --git a/solutions/quizzes/quiz1.rs b/solutions/quizzes/quiz1.rs new file mode 100644 index 00000000..bc761667 --- /dev/null +++ b/solutions/quizzes/quiz1.rs @@ -0,0 +1,31 @@ +// Mary is buying apples. The price of an apple is calculated as follows: +// - An apple costs 2 rustbucks. +// - If Mary buys more than 40 apples, each apple only costs 1 rustbuck! +// Write a function that calculates the price of an order of apples given the +// quantity bought. + +fn calculate_price_of_apples(n_apples: u64) -> u64 { + if n_apples > 40 { + n_apples + } else { + 2 * n_apples + } +} + +fn main() { + // You can optionally experiment here. +} + +// Don't change the tests! +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn verify_test() { + assert_eq!(calculate_price_of_apples(35), 70); + assert_eq!(calculate_price_of_apples(40), 80); + assert_eq!(calculate_price_of_apples(41), 41); + assert_eq!(calculate_price_of_apples(65), 65); + } +} diff --git a/solutions/quizzes/quiz2.rs b/solutions/quizzes/quiz2.rs new file mode 100644 index 00000000..0d2a5132 --- /dev/null +++ b/solutions/quizzes/quiz2.rs @@ -0,0 +1,107 @@ +// This is a quiz for the following sections: +// - Strings +// - Vecs +// - Move semantics +// - Modules +// - Enums +// +// Let's build a little machine in the form of a function. As input, we're going +// to give a list of strings and commands. These commands determine what action +// is going to be applied to the string. It can either be: +// - Uppercase the string +// - Trim the string +// - Append "bar" to the string a specified amount of times +// +// The exact form of this will be: +// - The input is going to be a vector of a 2-length tuple, +// the first element is the string, the second one is the command. +// - The output element is going to be a vector of strings. + +enum Command { + Uppercase, + Trim, + Append(usize), +} + +mod my_module { + use super::Command; + + // The solution with a loop. Check out `transformer_iter` for a version + // with iterators. + pub fn transformer(input: Vec<(String, Command)>) -> Vec { + let mut output = Vec::new(); + + for (mut string, command) in input { + // Create the new string. + let new_string = match command { + Command::Uppercase => string.to_uppercase(), + Command::Trim => string.trim().to_string(), + Command::Append(n) => { + for _ in 0..n { + string += "bar"; + } + string + } + }; + + // Push the new string to the output vector. + output.push(new_string); + } + + output + } + + // Equivalent to `transform` but uses an iterator instead of a loop for + // comparison. Don't worry, we will practice iterators later ;) + pub fn transformer_iter(input: Vec<(String, Command)>) -> Vec { + input + .into_iter() + .map(|(mut string, command)| match command { + Command::Uppercase => string.to_uppercase(), + Command::Trim => string.trim().to_string(), + Command::Append(n) => { + for _ in 0..n { + string += "bar"; + } + string + } + }) + .collect() + } +} + +fn main() { + // You can optionally experiment here. +} + +#[cfg(test)] +mod tests { + // Import `transformer`. + use super::my_module::transformer; + + use super::my_module::transformer_iter; + use super::Command; + + #[test] + fn it_works() { + for transformer in [transformer, transformer_iter] { + let input = vec![ + ("hello".to_string(), Command::Uppercase), + (" all roads lead to rome! ".to_string(), Command::Trim), + ("foo".to_string(), Command::Append(1)), + ("bar".to_string(), Command::Append(5)), + ]; + let output = transformer(input); + + assert_eq!( + output, + [ + "HELLO", + "all roads lead to rome!", + "foobar", + "barbarbarbarbarbar", + ] + ); + } + } +} diff --git a/solutions/quizzes/quiz3.rs b/solutions/quizzes/quiz3.rs new file mode 100644 index 00000000..e3413fd0 --- /dev/null +++ b/solutions/quizzes/quiz3.rs @@ -0,0 +1,69 @@ +// This quiz tests: +// - Generics +// - Traits +// +// An imaginary magical school has a new report card generation system written +// in Rust! Currently, the system only supports creating report cards where the +// student's grade is represented numerically (e.g. 1.0 -> 5.5). However, the +// school also issues alphabetical grades (A+ -> F-) and needs to be able to +// print both types of report card! +// +// Make the necessary code changes in the struct `ReportCard` and the impl +// block to support alphabetical report cards in addition to numerical ones. + +use std::fmt::Display; + +// Make the struct generic over `T`. +struct ReportCard { + // ^^^ + grade: T, + // ^ + student_name: String, + student_age: u8, +} + +// To be able to print the grade, it has to implement the `Display` trait. +impl ReportCard { + // ^^^^^^^ require that `T` implements `Display`. + fn print(&self) -> String { + format!( + "{} ({}) - achieved a grade of {}", + &self.student_name, &self.student_age, &self.grade, + ) + } +} + +fn main() { + // You can optionally experiment here. +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn generate_numeric_report_card() { + let report_card = ReportCard { + grade: 2.1, + student_name: "Tom Wriggle".to_string(), + student_age: 12, + }; + assert_eq!( + report_card.print(), + "Tom Wriggle (12) - achieved a grade of 2.1", + ); + } + + #[test] + fn generate_alphabetic_report_card() { + let report_card = ReportCard { + grade: "A+", + student_name: "Gary Plotter".to_string(), + student_age: 11, + }; + assert_eq!( + report_card.print(), + "Gary Plotter (11) - achieved a grade of A+", + ); + } +} diff --git a/src/app_state.rs b/src/app_state.rs new file mode 100644 index 00000000..e9a5b109 --- /dev/null +++ b/src/app_state.rs @@ -0,0 +1,502 @@ +use anyhow::{bail, Context, Result}; +use crossterm::style::Stylize; +use serde::Deserialize; +use std::{ + fs::{self, File}, + io::{Read, StdoutLock, Write}, + path::{Path, PathBuf}, + process::{Command, Stdio}, +}; + +use crate::{ + clear_terminal, + embedded::EMBEDDED_FILES, + exercise::{Exercise, RunnableExercise, OUTPUT_CAPACITY}, + info_file::ExerciseInfo, + DEBUG_PROFILE, +}; + +const STATE_FILE_NAME: &str = ".rustlings-state.txt"; +const BAD_INDEX_ERR: &str = "The current exercise index is higher than the number of exercises"; + +#[must_use] +pub enum ExercisesProgress { + // All exercises are done. + AllDone, + // The current exercise failed and is still pending. + CurrentPending, + // A new exercise is now pending. + NewPending, +} + +pub enum StateFileStatus { + Read, + NotRead, +} + +// Parses parts of the output of `cargo metadata`. +#[derive(Deserialize)] +struct CargoMetadata { + target_directory: PathBuf, +} + +pub fn parse_target_dir() -> Result { + // Get the target directory from Cargo. + let metadata_output = Command::new("cargo") + .arg("metadata") + .arg("-q") + .arg("--format-version") + .arg("1") + .arg("--no-deps") + .stdin(Stdio::null()) + .stderr(Stdio::inherit()) + .output() + .context(CARGO_METADATA_ERR)? + .stdout; + + serde_json::de::from_slice::(&metadata_output) + .context("Failed to read the field `target_directory` from the `cargo metadata` output") + .map(|metadata| metadata.target_directory) +} + +pub struct AppState { + current_exercise_ind: usize, + exercises: Vec, + // Caches the number of done exercises to avoid iterating over all exercises every time. + n_done: u16, + final_message: String, + // Preallocated buffer for reading and writing the state file. + file_buf: Vec, + official_exercises: bool, + // Cargo's target directory. + target_dir: PathBuf, +} + +impl AppState { + // Update the app state from the state file. + fn update_from_file(&mut self) -> StateFileStatus { + self.file_buf.clear(); + self.n_done = 0; + + if File::open(STATE_FILE_NAME) + .and_then(|mut file| file.read_to_end(&mut self.file_buf)) + .is_err() + { + return StateFileStatus::NotRead; + } + + // See `Self::write` for more information about the file format. + let mut lines = self.file_buf.split(|c| *c == b'\n').skip(2); + + let Some(current_exercise_name) = lines.next() else { + return StateFileStatus::NotRead; + }; + + if current_exercise_name.is_empty() || lines.next().is_none() { + return StateFileStatus::NotRead; + } + + let mut done_exercises = hashbrown::HashSet::with_capacity(self.exercises.len()); + + for done_exerise_name in lines { + if done_exerise_name.is_empty() { + break; + } + done_exercises.insert(done_exerise_name); + } + + for (ind, exercise) in self.exercises.iter_mut().enumerate() { + if done_exercises.contains(exercise.name.as_bytes()) { + exercise.done = true; + self.n_done += 1; + } + + if exercise.name.as_bytes() == current_exercise_name { + self.current_exercise_ind = ind; + } + } + + StateFileStatus::Read + } + + pub fn new( + exercise_infos: Vec, + final_message: String, + ) -> Result<(Self, StateFileStatus)> { + let target_dir = parse_target_dir()?; + + let exercises = exercise_infos + .into_iter() + .map(|exercise_info| { + // Leaking to be able to borrow in the watch mode `Table`. + // Leaking is not a problem because the `AppState` instance lives until + // the end of the program. + let path = exercise_info.path().leak(); + let name = exercise_info.name.leak(); + let dir = exercise_info.dir.map(|dir| &*dir.leak()); + + let hint = exercise_info.hint.trim().to_owned(); + + Exercise { + dir, + name, + path, + test: exercise_info.test, + strict_clippy: exercise_info.strict_clippy, + hint, + // Updated in `Self::update_from_file`. + done: false, + } + }) + .collect::>(); + + let mut slf = Self { + current_exercise_ind: 0, + exercises, + n_done: 0, + final_message, + file_buf: Vec::with_capacity(2048), + official_exercises: !Path::new("info.toml").exists(), + target_dir, + }; + + let state_file_status = slf.update_from_file(); + + Ok((slf, state_file_status)) + } + + #[inline] + pub fn current_exercise_ind(&self) -> usize { + self.current_exercise_ind + } + + #[inline] + pub fn exercises(&self) -> &[Exercise] { + &self.exercises + } + + #[inline] + pub fn n_done(&self) -> u16 { + self.n_done + } + + #[inline] + pub fn current_exercise(&self) -> &Exercise { + &self.exercises[self.current_exercise_ind] + } + + #[inline] + pub fn target_dir(&self) -> &Path { + &self.target_dir + } + + // Write the state file. + // The file's format is very simple: + // - The first line is a comment. + // - The second line is an empty line. + // - The third line is the name of the current exercise. It must end with `\n` even if there + // are no done exercises. + // - The fourth line is an empty line. + // - All remaining lines are the names of done exercises. + fn write(&mut self) -> Result<()> { + self.file_buf.clear(); + + self.file_buf + .extend_from_slice(b"DON'T EDIT THIS FILE!\n\n"); + self.file_buf + .extend_from_slice(self.current_exercise().name.as_bytes()); + self.file_buf.push(b'\n'); + + for exercise in &self.exercises { + if exercise.done { + self.file_buf.push(b'\n'); + self.file_buf.extend_from_slice(exercise.name.as_bytes()); + } + } + + fs::write(STATE_FILE_NAME, &self.file_buf) + .with_context(|| format!("Failed to write the state file {STATE_FILE_NAME}"))?; + + Ok(()) + } + + pub fn set_current_exercise_ind(&mut self, exercise_ind: usize) -> Result<()> { + if exercise_ind == self.current_exercise_ind { + return Ok(()); + } + + if exercise_ind >= self.exercises.len() { + bail!(BAD_INDEX_ERR); + } + + self.current_exercise_ind = exercise_ind; + + self.write() + } + + pub fn set_current_exercise_by_name(&mut self, name: &str) -> Result<()> { + // O(N) is fine since this method is used only once until the program exits. + // Building a hashmap would have more overhead. + self.current_exercise_ind = self + .exercises + .iter() + .position(|exercise| exercise.name == name) + .with_context(|| format!("No exercise found for '{name}'!"))?; + + self.write() + } + + pub fn set_pending(&mut self, exercise_ind: usize) -> Result<()> { + let exercise = self + .exercises + .get_mut(exercise_ind) + .context(BAD_INDEX_ERR)?; + + if exercise.done { + exercise.done = false; + self.n_done -= 1; + self.write()?; + } + + Ok(()) + } + + // Official exercises: Dump the original file from the binary. + // Third-party exercises: Reset the exercise file with `git stash`. + fn reset(&self, exercise_ind: usize, path: &str) -> Result<()> { + if self.official_exercises { + return EMBEDDED_FILES + .write_exercise_to_disk(exercise_ind, path) + .with_context(|| format!("Failed to reset the exercise {path}")); + } + + let output = Command::new("git") + .arg("stash") + .arg("push") + .arg("--") + .arg(path) + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .output() + .with_context(|| format!("Failed to run `git stash push -- {path}`"))?; + + if !output.status.success() { + bail!( + "`git stash push -- {path}` didn't run successfully: {}", + String::from_utf8_lossy(&output.stderr), + ); + } + + Ok(()) + } + + pub fn reset_current_exercise(&mut self) -> Result<&'static str> { + self.set_pending(self.current_exercise_ind)?; + let exercise = self.current_exercise(); + self.reset(self.current_exercise_ind, exercise.path)?; + + Ok(exercise.path) + } + + pub fn reset_exercise_by_ind(&mut self, exercise_ind: usize) -> Result<&'static str> { + if exercise_ind >= self.exercises.len() { + bail!(BAD_INDEX_ERR); + } + + self.set_pending(exercise_ind)?; + let exercise = &self.exercises[exercise_ind]; + self.reset(exercise_ind, exercise.path)?; + + Ok(exercise.path) + } + + // Return the index of the next pending exercise or `None` if all exercises are done. + fn next_pending_exercise_ind(&self) -> Option { + if self.current_exercise_ind == self.exercises.len() - 1 { + // The last exercise is done. + // Search for exercises not done from the start. + return self.exercises[..self.current_exercise_ind] + .iter() + .position(|exercise| !exercise.done); + } + + // The done exercise isn't the last one. + // Search for a pending exercise after the current one and then from the start. + match self.exercises[self.current_exercise_ind + 1..] + .iter() + .position(|exercise| !exercise.done) + { + Some(ind) => Some(self.current_exercise_ind + 1 + ind), + None => self.exercises[..self.current_exercise_ind] + .iter() + .position(|exercise| !exercise.done), + } + } + + /// Official exercises: Dump the solution file form the binary and return its path. + /// Third-party exercises: Check if a solution file exists and return its path in that case. + pub fn current_solution_path(&self) -> Result> { + if DEBUG_PROFILE { + return Ok(None); + } + + let current_exercise = self.current_exercise(); + + if self.official_exercises { + EMBEDDED_FILES + .write_solution_to_disk(self.current_exercise_ind, current_exercise.name) + .map(Some) + } else { + let solution_path = if let Some(dir) = current_exercise.dir { + format!("solutions/{dir}/{}.rs", current_exercise.name) + } else { + format!("solutions/{}.rs", current_exercise.name) + }; + + if Path::new(&solution_path).exists() { + return Ok(Some(solution_path)); + } + + Ok(None) + } + } + + /// Mark the current exercise as done and move on to the next pending exercise if one exists. + /// If all exercises are marked as done, run all of them to make sure that they are actually + /// done. If an exercise which is marked as done fails, mark it as pending and continue on it. + pub fn done_current_exercise(&mut self, writer: &mut StdoutLock) -> Result { + let exercise = &mut self.exercises[self.current_exercise_ind]; + if !exercise.done { + exercise.done = true; + self.n_done += 1; + } + + if let Some(ind) = self.next_pending_exercise_ind() { + self.set_current_exercise_ind(ind)?; + + return Ok(ExercisesProgress::NewPending); + } + + writer.write_all(RERUNNING_ALL_EXERCISES_MSG)?; + + let mut output = Vec::with_capacity(OUTPUT_CAPACITY); + for (exercise_ind, exercise) in self.exercises().iter().enumerate() { + write!(writer, "Running {exercise} ... ")?; + writer.flush()?; + + let success = exercise.run_exercise(&mut output, &self.target_dir)?; + if !success { + writeln!(writer, "{}\n", "FAILED".red())?; + + self.current_exercise_ind = exercise_ind; + + // No check if the exercise is done before setting it to pending + // because no pending exercise was found. + self.exercises[exercise_ind].done = false; + self.n_done -= 1; + + self.write()?; + + return Ok(ExercisesProgress::NewPending); + } + + writeln!(writer, "{}", "ok".green())?; + } + + clear_terminal(writer)?; + writer.write_all(FENISH_LINE.as_bytes())?; + + let final_message = self.final_message.trim(); + if !final_message.is_empty() { + writer.write_all(final_message.as_bytes())?; + writer.write_all(b"\n")?; + } + + Ok(ExercisesProgress::AllDone) + } +} + +const CARGO_METADATA_ERR: &str = "Failed to run the command `cargo metadata …` +Did you already install Rust? +Try running `cargo --version` to diagnose the problem."; + +const RERUNNING_ALL_EXERCISES_MSG: &[u8] = b" +All exercises seem to be done. +Recompiling and running all exercises to make sure that all of them are actually done. + +"; + +const FENISH_LINE: &str = "+----------------------------------------------------+ +| You made it to the Fe-nish line! | ++-------------------------- ------------------------+ + \\/\x1b[31m + ▒▒ ▒▒▒▒▒▒▒▒ ▒▒▒▒▒▒▒▒ ▒▒ + ▒▒▒▒ ▒▒ ▒▒ ▒▒ ▒▒ ▒▒ ▒▒ ▒▒▒▒ + ▒▒▒▒ ▒▒ ▒▒ ▒▒ ▒▒ ▒▒ ▒▒▒▒ + ░░▒▒▒▒░░▒▒ ▒▒ ▒▒ ▒▒ ▒▒░░▒▒▒▒ + ▓▓▓▓▓▓▓▓ ▓▓ ▓▓██ ▓▓ ▓▓██ ▓▓ ▓▓▓▓▓▓▓▓ + ▒▒▒▒ ▒▒ ████ ▒▒ ████ ▒▒░░ ▒▒▒▒ + ▒▒ ▒▒▒▒▒▒ ▒▒▒▒▒▒ ▒▒▒▒▒▒ ▒▒ + ▒▒▒▒▒▒▒▒▒▒▓▓▓▓▓▓▒▒▒▒▒▒▒▒▓▓▓▓▓▓▒▒▒▒▒▒▒▒ + ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ + ▒▒▒▒▒▒▒▒▒▒██▒▒▒▒▒▒██▒▒▒▒▒▒▒▒▒▒ + ▒▒ ▒▒▒▒▒▒▒▒▒▒██████▒▒▒▒▒▒▒▒▒▒ ▒▒ + ▒▒ ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ ▒▒ + ▒▒ ▒▒ ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ ▒▒ ▒▒ + ▒▒ ▒▒ ▒▒ ▒▒ ▒▒ ▒▒ + ▒▒ ▒▒ ▒▒ ▒▒\x1b[0m + +"; + +#[cfg(test)] +mod tests { + use super::*; + + fn dummy_exercise() -> Exercise { + Exercise { + dir: None, + name: "0", + path: "exercises/0.rs", + test: false, + strict_clippy: false, + hint: String::new(), + done: false, + } + } + + #[test] + fn next_pending_exercise() { + let mut app_state = AppState { + current_exercise_ind: 0, + exercises: vec![dummy_exercise(), dummy_exercise(), dummy_exercise()], + n_done: 0, + final_message: String::new(), + file_buf: Vec::new(), + official_exercises: true, + target_dir: PathBuf::new(), + }; + + let mut assert = |done: [bool; 3], expected: [Option; 3]| { + for (exercise, done) in app_state.exercises.iter_mut().zip(done) { + exercise.done = done; + } + for (ind, expected) in expected.into_iter().enumerate() { + app_state.current_exercise_ind = ind; + assert_eq!( + app_state.next_pending_exercise_ind(), + expected, + "done={done:?}, ind={ind}", + ); + } + }; + + assert([true, true, true], [None, None, None]); + assert([false, false, false], [Some(1), Some(2), Some(0)]); + assert([false, true, true], [None, Some(0), Some(0)]); + assert([true, false, true], [Some(1), None, Some(1)]); + assert([true, true, false], [Some(2), Some(2), None]); + assert([true, false, false], [Some(1), Some(2), Some(1)]); + assert([false, true, false], [Some(2), Some(2), Some(0)]); + assert([false, false, true], [Some(1), Some(0), Some(0)]); + } +} diff --git a/src/cargo_toml.rs b/src/cargo_toml.rs new file mode 100644 index 00000000..c4d6700a --- /dev/null +++ b/src/cargo_toml.rs @@ -0,0 +1,144 @@ +use anyhow::{Context, Result}; +use std::path::Path; + +use crate::info_file::ExerciseInfo; + +/// Initial capacity of the bins buffer. +pub const BINS_BUFFER_CAPACITY: usize = 1 << 14; + +/// Return the start and end index of the content of the list `bin = […]`. +/// bin = [xxxxxxxxxxxxxxxxx] +/// |start_ind | +/// |end_ind +pub fn bins_start_end_ind(cargo_toml: &str) -> Result<(usize, usize)> { + let start_ind = cargo_toml + .find("bin = [") + .context("Failed to find the start of the `bin` list (`bin = [`)")? + + 7; + let end_ind = start_ind + + cargo_toml + .get(start_ind..) + .and_then(|slice| slice.as_bytes().iter().position(|c| *c == b']')) + .context("Failed to find the end of the `bin` list (`]`)")?; + + Ok((start_ind, end_ind)) +} + +/// Generate and append the content of the `bin` list in `Cargo.toml`. +/// The `exercise_path_prefix` is the prefix of the `path` field of every list entry. +pub fn append_bins( + buf: &mut Vec, + exercise_infos: &[ExerciseInfo], + exercise_path_prefix: &[u8], +) { + buf.push(b'\n'); + for exercise_info in exercise_infos { + buf.extend_from_slice(b" { name = \""); + buf.extend_from_slice(exercise_info.name.as_bytes()); + buf.extend_from_slice(b"\", path = \""); + buf.extend_from_slice(exercise_path_prefix); + buf.extend_from_slice(b"exercises/"); + if let Some(dir) = &exercise_info.dir { + buf.extend_from_slice(dir.as_bytes()); + buf.push(b'/'); + } + buf.extend_from_slice(exercise_info.name.as_bytes()); + buf.extend_from_slice(b".rs\" },\n"); + + let sol_path = exercise_info.sol_path(); + if !Path::new(&sol_path).exists() { + continue; + } + + buf.extend_from_slice(b" { name = \""); + buf.extend_from_slice(exercise_info.name.as_bytes()); + buf.extend_from_slice(b"_sol"); + buf.extend_from_slice(b"\", path = \""); + buf.extend_from_slice(exercise_path_prefix); + buf.extend_from_slice(b"solutions/"); + if let Some(dir) = &exercise_info.dir { + buf.extend_from_slice(dir.as_bytes()); + buf.push(b'/'); + } + buf.extend_from_slice(exercise_info.name.as_bytes()); + buf.extend_from_slice(b".rs\" },\n"); + } +} + +/// Update the `bin` list and leave everything else unchanged. +pub fn updated_cargo_toml( + exercise_infos: &[ExerciseInfo], + current_cargo_toml: &str, + exercise_path_prefix: &[u8], +) -> Result> { + let (bins_start_ind, bins_end_ind) = bins_start_end_ind(current_cargo_toml)?; + + let mut updated_cargo_toml = Vec::with_capacity(BINS_BUFFER_CAPACITY); + updated_cargo_toml.extend_from_slice(current_cargo_toml[..bins_start_ind].as_bytes()); + append_bins( + &mut updated_cargo_toml, + exercise_infos, + exercise_path_prefix, + ); + updated_cargo_toml.extend_from_slice(current_cargo_toml[bins_end_ind..].as_bytes()); + + Ok(updated_cargo_toml) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_bins_start_end_ind() { + assert_eq!(bins_start_end_ind("").ok(), None); + assert_eq!(bins_start_end_ind("[]").ok(), None); + assert_eq!(bins_start_end_ind("bin = [").ok(), None); + assert_eq!(bins_start_end_ind("bin = ]").ok(), None); + assert_eq!(bins_start_end_ind("bin = []").ok(), Some((7, 7))); + assert_eq!(bins_start_end_ind("bin= []").ok(), None); + assert_eq!(bins_start_end_ind("bin =[]").ok(), None); + assert_eq!(bins_start_end_ind("bin=[]").ok(), None); + assert_eq!(bins_start_end_ind("bin = [\nxxx\n]").ok(), Some((7, 12))); + } + + #[test] + fn test_bins() { + let exercise_infos = [ + ExerciseInfo { + name: String::from("1"), + dir: None, + test: true, + strict_clippy: true, + hint: String::new(), + }, + ExerciseInfo { + name: String::from("2"), + dir: Some(String::from("d")), + test: false, + strict_clippy: false, + hint: String::new(), + }, + ]; + + let mut buf = Vec::with_capacity(128); + append_bins(&mut buf, &exercise_infos, b""); + assert_eq!( + buf, + br#" + { name = "1", path = "exercises/1.rs" }, + { name = "2", path = "exercises/d/2.rs" }, +"#, + ); + + assert_eq!( + updated_cargo_toml(&exercise_infos, "abc\nbin = [xxx]\n123", b"../").unwrap(), + br#"abc +bin = [ + { name = "1", path = "../exercises/1.rs" }, + { name = "2", path = "../exercises/d/2.rs" }, +] +123"#, + ); + } +} diff --git a/src/cmd.rs b/src/cmd.rs new file mode 100644 index 00000000..6092f531 --- /dev/null +++ b/src/cmd.rs @@ -0,0 +1,93 @@ +use anyhow::{Context, Result}; +use std::{io::Read, path::Path, process::Command}; + +/// Run a command with a description for a possible error and append the merged stdout and stderr. +/// The boolean in the returned `Result` is true if the command's exit status is success. +pub fn run_cmd(mut cmd: Command, description: &str, output: &mut Vec) -> Result { + let (mut reader, writer) = os_pipe::pipe() + .with_context(|| format!("Failed to create a pipe to run the command `{description}``"))?; + + let writer_clone = writer.try_clone().with_context(|| { + format!("Failed to clone the pipe writer for the command `{description}`") + })?; + + let mut handle = cmd + .stdout(writer_clone) + .stderr(writer) + .spawn() + .with_context(|| format!("Failed to run the command `{description}`"))?; + + // Prevent pipe deadlock. + drop(cmd); + + reader + .read_to_end(output) + .with_context(|| format!("Failed to read the output of the command `{description}`"))?; + + output.push(b'\n'); + + handle + .wait() + .with_context(|| format!("Failed to wait on the command `{description}` to exit")) + .map(|status| status.success()) +} + +pub struct CargoCmd<'a> { + pub subcommand: &'a str, + pub args: &'a [&'a str], + pub bin_name: &'a str, + pub description: &'a str, + /// RUSTFLAGS="-A warnings" + pub hide_warnings: bool, + /// Added as `--target-dir` if `Self::dev` is true. + pub target_dir: &'a Path, + /// The output buffer to append the merged stdout and stderr. + pub output: &'a mut Vec, + /// true while developing Rustlings. + pub dev: bool, +} + +impl<'a> CargoCmd<'a> { + /// Run `cargo SUBCOMMAND --bin EXERCISE_NAME … ARGS`. + pub fn run(&mut self) -> Result { + let mut cmd = Command::new("cargo"); + cmd.arg(self.subcommand); + + // A hack to make `cargo run` work when developing Rustlings. + if self.dev { + cmd.arg("--manifest-path") + .arg("dev/Cargo.toml") + .arg("--target-dir") + .arg(self.target_dir); + } + + cmd.arg("--color") + .arg("always") + .arg("-q") + .arg("--bin") + .arg(self.bin_name) + .args(self.args); + + if self.hide_warnings { + cmd.env("RUSTFLAGS", "-A warnings"); + } + + run_cmd(cmd, self.description, self.output) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_run_cmd() { + let mut cmd = Command::new("echo"); + cmd.arg("Hello"); + + let mut output = Vec::with_capacity(8); + run_cmd(cmd, "echo …", &mut output).unwrap(); + + assert_eq!(output, b"Hello\n\n"); + } +} diff --git a/src/dev.rs b/src/dev.rs new file mode 100644 index 00000000..5f7e64c8 --- /dev/null +++ b/src/dev.rs @@ -0,0 +1,48 @@ +use anyhow::{bail, Context, Result}; +use clap::Subcommand; +use std::path::PathBuf; + +use crate::DEBUG_PROFILE; + +mod check; +mod new; +mod update; + +#[derive(Subcommand)] +pub enum DevCommands { + /// Create a new project for third-party Rustlings exercises + New { + /// The path to create the project in + path: PathBuf, + /// Don't try to initialize a Git repository in the project directory + #[arg(long)] + no_git: bool, + }, + /// Run checks on the exercises + Check { + /// Require that every exercise has a solution + #[arg(short, long)] + require_solutions: bool, + }, + /// Update the `Cargo.toml` file for the exercises + Update, +} + +impl DevCommands { + pub fn run(self) -> Result<()> { + match self { + Self::New { path, no_git } => { + if DEBUG_PROFILE { + bail!("Disabled in the debug build"); + } + + new::new(&path, no_git).context(INIT_ERR) + } + Self::Check { require_solutions } => check::check(require_solutions), + Self::Update => update::update(), + } + } +} + +const INIT_ERR: &str = "Initialization failed. +After resolving the issue, delete the `rustlings` directory (if it was created) and try again"; diff --git a/src/dev/check.rs b/src/dev/check.rs new file mode 100644 index 00000000..5074c133 --- /dev/null +++ b/src/dev/check.rs @@ -0,0 +1,250 @@ +use anyhow::{anyhow, bail, Context, Result}; +use std::{ + cmp::Ordering, + fs::{self, read_dir, OpenOptions}, + io::{self, Read, Write}, + path::{Path, PathBuf}, + sync::{ + atomic::{self, AtomicBool}, + Mutex, + }, + thread, +}; + +use crate::{ + app_state::parse_target_dir, + cargo_toml::{append_bins, bins_start_end_ind, BINS_BUFFER_CAPACITY}, + exercise::{RunnableExercise, OUTPUT_CAPACITY}, + info_file::{ExerciseInfo, InfoFile}, + CURRENT_FORMAT_VERSION, DEBUG_PROFILE, +}; + +// Find a char that isn't allowed in the exercise's `name` or `dir`. +fn forbidden_char(input: &str) -> Option { + input.chars().find(|c| !c.is_alphanumeric() && *c != '_') +} + +// Check that the Cargo.toml file is up-to-date. +fn check_cargo_toml( + exercise_infos: &[ExerciseInfo], + current_cargo_toml: &str, + exercise_path_prefix: &[u8], +) -> Result<()> { + let (bins_start_ind, bins_end_ind) = bins_start_end_ind(current_cargo_toml)?; + + let old_bins = ¤t_cargo_toml.as_bytes()[bins_start_ind..bins_end_ind]; + let mut new_bins = Vec::with_capacity(BINS_BUFFER_CAPACITY); + append_bins(&mut new_bins, exercise_infos, exercise_path_prefix); + + if old_bins != new_bins { + if DEBUG_PROFILE { + bail!("The file `dev/Cargo.toml` is outdated. Please run `cargo run -- dev update` to update it"); + } + + bail!("The file `Cargo.toml` is outdated. Please run `rustlings dev update` to update it"); + } + + Ok(()) +} + +// Check the info of all exercises and return their paths in a set. +fn check_info_file_exercises(info_file: &InfoFile) -> Result> { + let mut names = hashbrown::HashSet::with_capacity(info_file.exercises.len()); + let mut paths = hashbrown::HashSet::with_capacity(info_file.exercises.len()); + + let mut file_buf = String::with_capacity(1 << 14); + for exercise_info in &info_file.exercises { + let name = exercise_info.name.as_str(); + if name.is_empty() { + bail!("Found an empty exercise name in `info.toml`"); + } + if let Some(c) = forbidden_char(name) { + bail!("Char `{c}` in the exercise name `{name}` is not allowed"); + } + + if let Some(dir) = &exercise_info.dir { + if dir.is_empty() { + bail!("The exercise `{name}` has an empty dir name in `info.toml`"); + } + if let Some(c) = forbidden_char(dir) { + bail!("Char `{c}` in the exercise dir `{dir}` is not allowed"); + } + } + + if exercise_info.hint.trim().is_empty() { + bail!("The exercise `{name}` has an empty hint. Please provide a hint or at least tell the user why a hint isn't needed for this exercise"); + } + + if !names.insert(name) { + bail!("The exercise name `{name}` is duplicated. Exercise names must all be unique"); + } + + let path = exercise_info.path(); + + OpenOptions::new() + .read(true) + .open(&path) + .with_context(|| format!("Failed to open the file {path}"))? + .read_to_string(&mut file_buf) + .with_context(|| format!("Failed to read the file {path}"))?; + + if !file_buf.contains("fn main()") { + bail!("The `main` function is missing in the file `{path}`.\nCreate at least an empty `main` function to avoid language server errors"); + } + + if !exercise_info.test && file_buf.contains("#[test]") { + bail!("The file `{path}` contains tests annotated with `#[test]` but the exercise `{name}` has `test = false` in the `info.toml` file"); + } + + file_buf.clear(); + + paths.insert(PathBuf::from(path)); + } + + Ok(paths) +} + +// Check `dir` for unexpected files. +// Only Rust files in `allowed_rust_files` and `README.md` files are allowed. +// Only one level of directory nesting is allowed. +fn check_unexpected_files( + dir: &str, + allowed_rust_files: &hashbrown::HashSet, +) -> Result<()> { + let unexpected_file = |path: &Path| { + anyhow!("Found the file `{}`. Only `README.md` and Rust files related to an exercise in `info.toml` are allowed in the `{dir}` directory", path.display()) + }; + + for entry in read_dir(dir).with_context(|| format!("Failed to open the `{dir}` directory"))? { + let entry = entry.with_context(|| format!("Failed to read the `{dir}` directory"))?; + + if entry.file_type().unwrap().is_file() { + let path = entry.path(); + let file_name = path.file_name().unwrap(); + if file_name == "README.md" { + continue; + } + + if !allowed_rust_files.contains(&path) { + return Err(unexpected_file(&path)); + } + + continue; + } + + let dir_path = entry.path(); + for entry in read_dir(&dir_path) + .with_context(|| format!("Failed to open the directory {}", dir_path.display()))? + { + let entry = entry + .with_context(|| format!("Failed to read the directory {}", dir_path.display()))?; + let path = entry.path(); + + if !entry.file_type().unwrap().is_file() { + bail!("Found `{}` but expected only files. Only one level of exercise nesting is allowed", path.display()); + } + + let file_name = path.file_name().unwrap(); + if file_name == "README.md" { + continue; + } + + if !allowed_rust_files.contains(&path) { + return Err(unexpected_file(&path)); + } + } + } + + Ok(()) +} + +fn check_exercises(info_file: &InfoFile) -> Result<()> { + match info_file.format_version.cmp(&CURRENT_FORMAT_VERSION) { + Ordering::Less => bail!("`format_version` < {CURRENT_FORMAT_VERSION} (supported version)\nPlease migrate to the latest format version"), + Ordering::Greater => bail!("`format_version` > {CURRENT_FORMAT_VERSION} (supported version)\nTry updating the Rustlings program"), + Ordering::Equal => (), + } + + let info_file_paths = check_info_file_exercises(info_file)?; + check_unexpected_files("exercises", &info_file_paths)?; + + Ok(()) +} + +fn check_solutions(require_solutions: bool, info_file: &InfoFile) -> Result<()> { + let target_dir = parse_target_dir()?; + let paths = Mutex::new(hashbrown::HashSet::with_capacity(info_file.exercises.len())); + let error_occurred = AtomicBool::new(false); + + println!("Running all solutions. This may take a while...\n"); + thread::scope(|s| { + for exercise_info in &info_file.exercises { + s.spawn(|| { + let error = |e| { + let mut stderr = io::stderr().lock(); + stderr.write_all(e).unwrap(); + stderr + .write_all(b"\nFailed to run the solution of the exercise ") + .unwrap(); + stderr.write_all(exercise_info.name.as_bytes()).unwrap(); + stderr.write_all(SEPARATOR).unwrap(); + error_occurred.store(true, atomic::Ordering::Relaxed); + }; + + let path = exercise_info.sol_path(); + if !Path::new(&path).exists() { + if require_solutions { + error(b"Solution missing"); + } + + // No solution to check. + return; + } + + let mut output = Vec::with_capacity(OUTPUT_CAPACITY); + match exercise_info.run_solution(&mut output, &target_dir) { + Ok(true) => { + paths.lock().unwrap().insert(PathBuf::from(path)); + } + Ok(false) => error(&output), + Err(e) => error(e.to_string().as_bytes()), + } + }); + } + }); + + if error_occurred.load(atomic::Ordering::Relaxed) { + bail!("At least one solution failed. See the output above."); + } + + check_unexpected_files("solutions", &paths.into_inner().unwrap())?; + + Ok(()) +} + +pub fn check(require_solutions: bool) -> Result<()> { + let info_file = InfoFile::parse()?; + + // A hack to make `cargo run -- dev check` work when developing Rustlings. + if DEBUG_PROFILE { + check_cargo_toml( + &info_file.exercises, + include_str!("../../dev-Cargo.toml"), + b"../", + )?; + } else { + let current_cargo_toml = + fs::read_to_string("Cargo.toml").context("Failed to read the file `Cargo.toml`")?; + check_cargo_toml(&info_file.exercises, ¤t_cargo_toml, b"")?; + } + + check_exercises(&info_file)?; + check_solutions(require_solutions, &info_file)?; + + println!("\nEverything looks fine!"); + + Ok(()) +} + +const SEPARATOR: &[u8] = + b"\n========================================================================================\n"; diff --git a/src/dev/new.rs b/src/dev/new.rs new file mode 100644 index 00000000..fefc4fc1 --- /dev/null +++ b/src/dev/new.rs @@ -0,0 +1,145 @@ +use anyhow::{bail, Context, Result}; +use std::{ + env::set_current_dir, + fs::{self, create_dir}, + path::Path, + process::Command, +}; + +use crate::CURRENT_FORMAT_VERSION; + +// Create a directory relative to the current directory and print its path. +fn create_rel_dir(dir_name: &str, current_dir: &str) -> Result<()> { + create_dir(dir_name) + .with_context(|| format!("Failed to create the directory {current_dir}/{dir_name}"))?; + println!("Created the directory {current_dir}/{dir_name}"); + Ok(()) +} + +// Write a file relative to the current directory and print its path. +fn write_rel_file(file_name: &str, current_dir: &str, content: C) -> Result<()> +where + C: AsRef<[u8]>, +{ + fs::write(file_name, content) + .with_context(|| format!("Failed to create the file {current_dir}/{file_name}"))?; + // Space to align with `create_rel_dir`. + println!("Created the file {current_dir}/{file_name}"); + Ok(()) +} + +pub fn new(path: &Path, no_git: bool) -> Result<()> { + let dir_path_str = path.to_string_lossy(); + + create_dir(path).with_context(|| format!("Failed to create the directory {dir_path_str}"))?; + println!("Created the directory {dir_path_str}"); + + set_current_dir(path) + .with_context(|| format!("Failed to set {dir_path_str} as the current directory"))?; + + if !no_git + && !Command::new("git") + .arg("init") + .status() + .context("Failed to run `git init`")? + .success() + { + bail!("`git init` didn't run successfully. See the possible error message above"); + } + + write_rel_file(".gitignore", &dir_path_str, GITIGNORE)?; + + create_rel_dir("exercises", &dir_path_str)?; + create_rel_dir("solutions", &dir_path_str)?; + + write_rel_file( + "info.toml", + &dir_path_str, + format!("{INFO_FILE_BEFORE_FORMAT_VERSION}{CURRENT_FORMAT_VERSION}{INFO_FILE_AFTER_FORMAT_VERSION}"), + )?; + + write_rel_file("Cargo.toml", &dir_path_str, CARGO_TOML)?; + + write_rel_file("README.md", &dir_path_str, README)?; + + create_rel_dir(".vscode", &dir_path_str)?; + write_rel_file( + ".vscode/extensions.json", + &dir_path_str, + crate::init::VS_CODE_EXTENSIONS_JSON, + )?; + + println!("\nInitialization done ✓"); + + Ok(()) +} + +pub const GITIGNORE: &[u8] = b".rustlings-state.txt +Cargo.lock +target +.vscode +!.vscode/extensions.json +"; + +const INFO_FILE_BEFORE_FORMAT_VERSION: &str = + "# The format version is an indicator of the compatibility of third-party exercises with the +# Rustlings program. +# The format version is not the same as the version of the Rustlings program. +# In case Rustlings makes an unavoidable breaking change to the expected format of third-party +# exercises, you would need to raise this version and adapt to the new format. +# Otherwise, the newest version of the Rustlings program won't be able to run these exercises. +format_version = "; + +const INFO_FILE_AFTER_FORMAT_VERSION: &str = r#" + +# Optional multi-line message to be shown to users when just starting with the exercises. +welcome_message = """Welcome to these third-party Rustlings exercises.""" + +# Optional multi-line message to be shown to users after finishing all exercises. +final_message = """We hope that you found the exercises helpful :D""" + +# Repeat this section for every exercise. +[[exercises]] +# Exercise name which is the exercise file name without the `.rs` extension. +name = "???" + +# Optional directory name to be provided if you want to organize exercises in directories. +# If `dir` is specified, the exercise path is `exercises/DIR/NAME.rs` +# Otherwise, the path is `exercises/NAME.rs` +# dir = "???" + +# Rustlings expects the exercise to contain tests and run them. +# You can optionally disable testing by setting `test` to `false` (the default is `true`). +# In that case, the exercise will be considered done when it just successfully compiles. +# test = true + +# Rustlings will always run Clippy on exercises. +# You can optionally set `strict_clippy` to `true` (the default is `false`) to only consider +# the exercise as done when there are no warnings left. +# strict_clippy = false + +# A multi-line hint to be shown to users on request. +hint = """???""" +"#; + +const CARGO_TOML: &[u8] = + br#"# Don't edit the `bin` list manually! It is updated by `rustlings dev update` +bin = [] + +[package] +name = "exercises" +edition = "2021" +# Don't publish the exercises on crates.io! +publish = false + +[dependencies] +"#; + +const README: &str = "# Rustlings 🦀 + +Welcome to these third-party Rustlings exercises 😃 + +First, [install Rustlings using the official instructions in the README of the Rustlings project](https://github.com/rust-lang/rustlings) ✅ + +Then, open your terminal in this directory and run `rustlings` to get started with the exercises 🚀 +"; diff --git a/src/dev/update.rs b/src/dev/update.rs new file mode 100644 index 00000000..66efe3d0 --- /dev/null +++ b/src/dev/update.rs @@ -0,0 +1,50 @@ +use anyhow::{Context, Result}; +use std::fs; + +use crate::{ + cargo_toml::updated_cargo_toml, + info_file::{ExerciseInfo, InfoFile}, + DEBUG_PROFILE, +}; + +// Update the `Cargo.toml` file. +fn update_cargo_toml( + exercise_infos: &[ExerciseInfo], + current_cargo_toml: &str, + exercise_path_prefix: &[u8], + cargo_toml_path: &str, +) -> Result<()> { + let updated_cargo_toml = + updated_cargo_toml(exercise_infos, current_cargo_toml, exercise_path_prefix)?; + + fs::write(cargo_toml_path, updated_cargo_toml) + .context("Failed to write the `Cargo.toml` file")?; + + Ok(()) +} + +pub fn update() -> Result<()> { + let info_file = InfoFile::parse()?; + + // A hack to make `cargo run -- dev update` work when developing Rustlings. + if DEBUG_PROFILE { + update_cargo_toml( + &info_file.exercises, + include_str!("../../dev-Cargo.toml"), + b"../", + "dev/Cargo.toml", + ) + .context("Failed to update the file `dev/Cargo.toml`")?; + + println!("Updated `dev/Cargo.toml`"); + } else { + let current_cargo_toml = + fs::read_to_string("Cargo.toml").context("Failed to read the file `Cargo.toml`")?; + update_cargo_toml(&info_file.exercises, ¤t_cargo_toml, b"", "Cargo.toml") + .context("Failed to update the file `Cargo.toml`")?; + + println!("Updated `Cargo.toml`"); + } + + Ok(()) +} diff --git a/src/embedded.rs b/src/embedded.rs new file mode 100644 index 00000000..1dce46c5 --- /dev/null +++ b/src/embedded.rs @@ -0,0 +1,168 @@ +use anyhow::{Context, Error, Result}; +use std::{ + fs::{create_dir, OpenOptions}, + io::{self, Write}, +}; + +use crate::info_file::ExerciseInfo; + +/// Contains all embedded files. +pub static EMBEDDED_FILES: EmbeddedFiles = rustlings_macros::include_files!(); + +#[derive(Clone, Copy)] +pub enum WriteStrategy { + IfNotExists, + Overwrite, +} + +impl WriteStrategy { + fn write(self, path: &str, content: &[u8]) -> Result<()> { + let file = match self { + Self::IfNotExists => OpenOptions::new().create_new(true).write(true).open(path), + Self::Overwrite => OpenOptions::new() + .create(true) + .write(true) + .truncate(true) + .open(path), + }; + + file.with_context(|| format!("Failed to open the file `{path}` in write mode"))? + .write_all(content) + .with_context(|| format!("Failed to write the file {path}")) + } +} + +// Files related to one exercise. +struct ExerciseFiles { + // The content of the exercise file. + exercise: &'static [u8], + // The content of the solution file. + solution: &'static [u8], + // Index of the related `ExerciseDir` in `EmbeddedFiles::exercise_dirs`. + dir_ind: usize, +} + +// A directory in the `exercises/` directory. +pub struct ExerciseDir { + pub name: &'static str, + readme: &'static [u8], +} + +impl ExerciseDir { + fn init_on_disk(&self) -> Result<()> { + // 20 = 10 + 10 + // exercises/ + /README.md + let mut dir_path = String::with_capacity(20 + self.name.len()); + dir_path.push_str("exercises/"); + dir_path.push_str(self.name); + + if let Err(e) = create_dir(&dir_path) { + if e.kind() == io::ErrorKind::AlreadyExists { + return Ok(()); + } + + return Err( + Error::from(e).context(format!("Failed to create the directory {dir_path}")) + ); + } + + let mut readme_path = dir_path; + readme_path.push_str("/README.md"); + + WriteStrategy::Overwrite.write(&readme_path, self.readme) + } +} + +/// All embedded files. +pub struct EmbeddedFiles { + /// The content of the `info.toml` file. + pub info_file: &'static str, + exercise_files: &'static [ExerciseFiles], + pub exercise_dirs: &'static [ExerciseDir], +} + +impl EmbeddedFiles { + /// Dump all the embedded files of the `exercises/` directory. + pub fn init_exercises_dir(&self, exercise_infos: &[ExerciseInfo]) -> Result<()> { + create_dir("exercises").context("Failed to create the directory `exercises`")?; + + WriteStrategy::IfNotExists.write( + "exercises/README.md", + include_bytes!("../exercises/README.md"), + )?; + + for dir in self.exercise_dirs { + dir.init_on_disk()?; + } + + for (exercise_info, exercise_files) in exercise_infos.iter().zip(self.exercise_files) { + WriteStrategy::IfNotExists.write(&exercise_info.path(), exercise_files.exercise)?; + } + + Ok(()) + } + + pub fn write_exercise_to_disk(&self, exercise_ind: usize, path: &str) -> Result<()> { + let exercise_files = &self.exercise_files[exercise_ind]; + let dir = &self.exercise_dirs[exercise_files.dir_ind]; + + dir.init_on_disk()?; + WriteStrategy::Overwrite.write(path, exercise_files.exercise) + } + + /// Write the solution file to disk and return its path. + pub fn write_solution_to_disk( + &self, + exercise_ind: usize, + exercise_name: &str, + ) -> Result { + let exercise_files = &self.exercise_files[exercise_ind]; + let dir = &self.exercise_dirs[exercise_files.dir_ind]; + + // 14 = 10 + 1 + 3 + // solutions/ + / + .rs + let mut solution_path = String::with_capacity(14 + dir.name.len() + exercise_name.len()); + solution_path.push_str("solutions/"); + solution_path.push_str(dir.name); + solution_path.push('/'); + solution_path.push_str(exercise_name); + solution_path.push_str(".rs"); + + WriteStrategy::Overwrite.write(&solution_path, exercise_files.solution)?; + + Ok(solution_path) + } +} + +#[cfg(test)] +mod tests { + use serde::Deserialize; + + use super::*; + + #[derive(Deserialize)] + struct ExerciseInfo { + dir: String, + } + + #[derive(Deserialize)] + struct InfoFile { + exercises: Vec, + } + + #[test] + fn dirs() { + let exercises = toml_edit::de::from_str::(EMBEDDED_FILES.info_file) + .expect("Failed to parse `info.toml`") + .exercises; + + assert_eq!(exercises.len(), EMBEDDED_FILES.exercise_files.len()); + + for (exercise, exercise_files) in exercises.iter().zip(EMBEDDED_FILES.exercise_files) { + assert_eq!( + exercise.dir, + EMBEDDED_FILES.exercise_dirs[exercise_files.dir_ind].name, + ); + } + } +} diff --git a/src/exercise.rs b/src/exercise.rs index 19f528a8..b6adc141 100644 --- a/src/exercise.rs +++ b/src/exercise.rs @@ -1,464 +1,181 @@ -use serde::Deserialize; -use std::fmt::{self, Display, Formatter}; -use std::fs::{self, remove_file, File}; -use std::io::{self, BufRead, BufReader}; -use std::path::PathBuf; -use std::process::{self, exit, Command, Stdio}; -use std::{array, env, mem}; -use winnow::ascii::{space0, Caseless}; -use winnow::combinator::opt; -use winnow::Parser; +use anyhow::Result; +use crossterm::style::{style, StyledContent, Stylize}; +use std::{ + fmt::{self, Display, Formatter}, + io::Write, + path::{Path, PathBuf}, + process::Command, +}; -const RUSTC_COLOR_ARGS: &[&str] = &["--color", "always"]; -const RUSTC_EDITION_ARGS: &[&str] = &["--edition", "2021"]; -const RUSTC_NO_DEBUG_ARGS: &[&str] = &["-C", "strip=debuginfo"]; -const CONTEXT: usize = 2; -const CLIPPY_CARGO_TOML_PATH: &str = "./exercises/22_clippy/Cargo.toml"; +use crate::{ + cmd::{run_cmd, CargoCmd}, + in_official_repo, + terminal_link::TerminalFileLink, + DEBUG_PROFILE, +}; -// Checks if the line contains the "I AM NOT DONE" comment. -fn contains_not_done_comment(input: &str) -> bool { - ( - space0::<_, ()>, - "//", - opt('/'), - space0, - Caseless("I AM NOT DONE"), - ) - .parse_next(&mut &*input) - .is_ok() +/// The initial capacity of the output buffer. +pub const OUTPUT_CAPACITY: usize = 1 << 14; + +// Run an exercise binary and append its output to the `output` buffer. +// Compilation must be done before calling this method. +fn run_bin(bin_name: &str, output: &mut Vec, target_dir: &Path) -> Result { + writeln!(output, "{}", "Output".underlined())?; + + // 7 = "/debug/".len() + let mut bin_path = PathBuf::with_capacity(target_dir.as_os_str().len() + 7 + bin_name.len()); + bin_path.push(target_dir); + bin_path.push("debug"); + bin_path.push(bin_name); + + let success = run_cmd(Command::new(&bin_path), &bin_path.to_string_lossy(), output)?; + + if !success { + // This output is important to show the user that something went wrong. + // Otherwise, calling something like `exit(1)` in an exercise without further output + // leaves the user confused about why the exercise isn't done yet. + writeln!( + output, + "{}", + "The exercise didn't run successfully (nonzero exit code)" + .bold() + .red(), + )?; + } + + Ok(success) } -// Get a temporary file name that is hopefully unique -#[inline] -fn temp_file() -> String { - let thread_id: String = format!("{:?}", std::thread::current().id()) - .chars() - .filter(|c| c.is_alphanumeric()) - .collect(); - - format!("./temp_{}_{thread_id}", process::id()) -} - -// The mode of the exercise. -#[derive(Deserialize, Copy, Clone, Debug)] -#[serde(rename_all = "lowercase")] -pub enum Mode { - // Indicates that the exercise should be compiled as a binary - Compile, - // Indicates that the exercise should be compiled as a test harness - Test, - // Indicates that the exercise should be linted with clippy - Clippy, -} - -#[derive(Deserialize)] -pub struct ExerciseList { - pub exercises: Vec, -} - -// A representation of a rustlings exercise. -// This is deserialized from the accompanying info.toml file -#[derive(Deserialize, Debug)] +/// See `info_file::ExerciseInfo` pub struct Exercise { - // Name of the exercise - pub name: String, - // The path to the file containing the exercise's source code - pub path: PathBuf, - // The mode of the exercise (Test, Compile, or Clippy) - pub mode: Mode, - // The hint text associated with the exercise + pub dir: Option<&'static str>, + pub name: &'static str, + /// Path of the exercise file starting with the `exercises/` directory. + pub path: &'static str, + pub test: bool, + pub strict_clippy: bool, pub hint: String, -} - -// An enum to track of the state of an Exercise. -// An Exercise can be either Done or Pending -#[derive(PartialEq, Eq, Debug)] -pub enum State { - // The state of the exercise once it's been completed - Done, - // The state of the exercise while it's not completed yet - Pending(Vec), -} - -// The context information of a pending exercise -#[derive(PartialEq, Eq, Debug)] -pub struct ContextLine { - // The source code that is still pending completion - pub line: String, - // The line number of the source code still pending completion - pub number: usize, - // Whether or not this is important - pub important: bool, -} - -// The result of compiling an exercise -pub struct CompiledExercise<'a> { - exercise: &'a Exercise, - _handle: FileHandle, -} - -impl<'a> CompiledExercise<'a> { - // Run the compiled exercise - pub fn run(&self) -> Result { - self.exercise.run() - } -} - -// A representation of an already executed binary -#[derive(Debug)] -pub struct ExerciseOutput { - // The textual contents of the standard output of the binary - pub stdout: String, - // The textual contents of the standard error of the binary - pub stderr: String, -} - -struct FileHandle; - -impl Drop for FileHandle { - fn drop(&mut self) { - clean(); - } + pub done: bool, } impl Exercise { - pub fn compile(&self) -> Result { - let cmd = match self.mode { - Mode::Compile => Command::new("rustc") - .args([self.path.to_str().unwrap(), "-o", &temp_file()]) - .args(RUSTC_COLOR_ARGS) - .args(RUSTC_EDITION_ARGS) - .args(RUSTC_NO_DEBUG_ARGS) - .output(), - Mode::Test => Command::new("rustc") - .args(["--test", self.path.to_str().unwrap(), "-o", &temp_file()]) - .args(RUSTC_COLOR_ARGS) - .args(RUSTC_EDITION_ARGS) - .args(RUSTC_NO_DEBUG_ARGS) - .output(), - Mode::Clippy => { - let cargo_toml = format!( - r#"[package] -name = "{}" -version = "0.0.1" -edition = "2021" -[[bin]] -name = "{}" -path = "{}.rs""#, - self.name, self.name, self.name - ); - let cargo_toml_error_msg = if env::var("NO_EMOJI").is_ok() { - "Failed to write Clippy Cargo.toml file." - } else { - "Failed to write 📎 Clippy 📎 Cargo.toml file." - }; - fs::write(CLIPPY_CARGO_TOML_PATH, cargo_toml).expect(cargo_toml_error_msg); - // To support the ability to run the clippy exercises, build - // an executable, in addition to running clippy. With a - // compilation failure, this would silently fail. But we expect - // clippy to reflect the same failure while compiling later. - Command::new("rustc") - .args([self.path.to_str().unwrap(), "-o", &temp_file()]) - .args(RUSTC_COLOR_ARGS) - .args(RUSTC_EDITION_ARGS) - .args(RUSTC_NO_DEBUG_ARGS) - .stdin(Stdio::null()) - .stdout(Stdio::null()) - .stderr(Stdio::null()) - .status() - .expect("Failed to compile!"); - // Due to an issue with Clippy, a cargo clean is required to catch all lints. - // See https://github.com/rust-lang/rust-clippy/issues/2604 - // This is already fixed on Clippy's master branch. See this issue to track merging into Cargo: - // https://github.com/rust-lang/rust-clippy/issues/3837 - Command::new("cargo") - .args(["clean", "--manifest-path", CLIPPY_CARGO_TOML_PATH]) - .args(RUSTC_COLOR_ARGS) - .stdin(Stdio::null()) - .stdout(Stdio::null()) - .stderr(Stdio::null()) - .status() - .expect("Failed to run 'cargo clean'"); - Command::new("cargo") - .args(["clippy", "--manifest-path", CLIPPY_CARGO_TOML_PATH]) - .args(RUSTC_COLOR_ARGS) - .args(["--", "-D", "warnings", "-D", "clippy::float_cmp"]) - .output() - } - } - .expect("Failed to run 'compile' command."); - - if cmd.status.success() { - Ok(CompiledExercise { - exercise: self, - _handle: FileHandle, - }) - } else { - clean(); - Err(ExerciseOutput { - stdout: String::from_utf8_lossy(&cmd.stdout).to_string(), - stderr: String::from_utf8_lossy(&cmd.stderr).to_string(), - }) - } - } - - fn run(&self) -> Result { - let arg = match self.mode { - Mode::Test => "--show-output", - _ => "", - }; - let cmd = Command::new(temp_file()) - .arg(arg) - .output() - .expect("Failed to run 'run' command"); - - let output = ExerciseOutput { - stdout: String::from_utf8_lossy(&cmd.stdout).to_string(), - stderr: String::from_utf8_lossy(&cmd.stderr).to_string(), - }; - - if cmd.status.success() { - Ok(output) - } else { - Err(output) - } - } - - pub fn state(&self) -> State { - let source_file = File::open(&self.path).unwrap_or_else(|e| { - println!( - "Failed to open the exercise file {}: {e}", - self.path.display(), - ); - exit(1); - }); - let mut source_reader = BufReader::new(source_file); - - // Read the next line into `buf` without the newline at the end. - let mut read_line = |buf: &mut String| -> io::Result<_> { - let n = source_reader.read_line(buf)?; - if buf.ends_with('\n') { - buf.pop(); - if buf.ends_with('\r') { - buf.pop(); - } - } - Ok(n) - }; - - let mut current_line_number: usize = 1; - // Keep the last `CONTEXT` lines while iterating over the file lines. - let mut prev_lines: [_; CONTEXT] = array::from_fn(|_| String::with_capacity(256)); - let mut line = String::with_capacity(256); - - loop { - let n = read_line(&mut line).unwrap_or_else(|e| { - println!( - "Failed to read the exercise file {}: {e}", - self.path.display(), - ); - exit(1); - }); - - // Reached the end of the file and didn't find the comment. - if n == 0 { - return State::Done; - } - - if contains_not_done_comment(&line) { - let mut context = Vec::with_capacity(2 * CONTEXT + 1); - // Previous lines. - for (ind, prev_line) in prev_lines - .into_iter() - .take(current_line_number - 1) - .enumerate() - .rev() - { - context.push(ContextLine { - line: prev_line, - number: current_line_number - 1 - ind, - important: false, - }); - } - - // Current line. - context.push(ContextLine { - line, - number: current_line_number, - important: true, - }); - - // Next lines. - for ind in 0..CONTEXT { - let mut next_line = String::with_capacity(256); - let Ok(n) = read_line(&mut next_line) else { - // If an error occurs, just ignore the next lines. - break; - }; - - // Reached the end of the file. - if n == 0 { - break; - } - - context.push(ContextLine { - line: next_line, - number: current_line_number + 1 + ind, - important: false, - }); - } - - return State::Pending(context); - } - - current_line_number += 1; - // Add the current line as a previous line and shift the older lines by one. - for prev_line in &mut prev_lines { - mem::swap(&mut line, prev_line); - } - // The current line now contains the oldest previous line. - // Recycle it for reading the next line. - line.clear(); - } - } - - // Check that the exercise looks to be solved using self.state() - // This is not the best way to check since - // the user can just remove the "I AM NOT DONE" string from the file - // without actually having solved anything. - // The only other way to truly check this would to compile and run - // the exercise; which would be both costly and counterintuitive - pub fn looks_done(&self) -> bool { - self.state() == State::Done + pub fn terminal_link(&self) -> StyledContent> { + style(TerminalFileLink(self.path)).underlined().blue() } } impl Display for Exercise { fn fmt(&self, f: &mut Formatter) -> fmt::Result { - write!(f, "{}", self.path.to_str().unwrap()) + self.path.fmt(f) } } -#[inline] -fn clean() { - let _ignored = remove_file(temp_file()); +pub trait RunnableExercise { + fn name(&self) -> &str; + fn strict_clippy(&self) -> bool; + fn test(&self) -> bool; + + // Compile, check and run the exercise or its solution (depending on `bin_name´). + // The output is written to the `output` buffer after clearing it. + fn run(&self, bin_name: &str, output: &mut Vec, target_dir: &Path) -> Result { + output.clear(); + + // Developing the official Rustlings. + let dev = DEBUG_PROFILE && in_official_repo(); + + let build_success = CargoCmd { + subcommand: "build", + args: &[], + bin_name, + description: "cargo build …", + hide_warnings: false, + target_dir, + output, + dev, + } + .run()?; + if !build_success { + return Ok(false); + } + + // Discard the output of `cargo build` because it will be shown again by Clippy. + output.clear(); + + // `--profile test` is required to also check code with `[cfg(test)]`. + let clippy_args: &[&str] = if self.strict_clippy() { + &["--profile", "test", "--", "-D", "warnings"] + } else { + &["--profile", "test"] + }; + let clippy_success = CargoCmd { + subcommand: "clippy", + args: clippy_args, + bin_name, + description: "cargo clippy …", + hide_warnings: false, + target_dir, + output, + dev, + } + .run()?; + if !clippy_success { + return Ok(false); + } + + if !self.test() { + return run_bin(bin_name, output, target_dir); + } + + let test_success = CargoCmd { + subcommand: "test", + args: &["--", "--color", "always", "--show-output"], + bin_name, + description: "cargo test …", + // Hide warnings because they are shown by Clippy. + hide_warnings: true, + target_dir, + output, + dev, + } + .run()?; + + let run_success = run_bin(bin_name, output, target_dir)?; + + Ok(test_success && run_success) + } + + /// Compile, check and run the exercise. + /// The output is written to the `output` buffer after clearing it. + #[inline] + fn run_exercise(&self, output: &mut Vec, target_dir: &Path) -> Result { + self.run(self.name(), output, target_dir) + } + + /// Compile, check and run the exercise's solution. + /// The output is written to the `output` buffer after clearing it. + fn run_solution(&self, output: &mut Vec, target_dir: &Path) -> Result { + let name = self.name(); + let mut bin_name = String::with_capacity(name.len()); + bin_name.push_str(name); + bin_name.push_str("_sol"); + + self.run(&bin_name, output, target_dir) + } } -#[cfg(test)] -mod test { - use super::*; - use std::path::Path; - - #[test] - fn test_clean() { - File::create(temp_file()).unwrap(); - let exercise = Exercise { - name: String::from("example"), - path: PathBuf::from("tests/fixture/state/pending_exercise.rs"), - mode: Mode::Compile, - hint: String::from(""), - }; - let compiled = exercise.compile().unwrap(); - drop(compiled); - assert!(!Path::new(&temp_file()).exists()); +impl RunnableExercise for Exercise { + #[inline] + fn name(&self) -> &str { + self.name } - #[test] - #[cfg(target_os = "windows")] - fn test_no_pdb_file() { - [Mode::Compile, Mode::Test] // Clippy doesn't like to test - .iter() - .for_each(|mode| { - let exercise = Exercise { - name: String::from("example"), - // We want a file that does actually compile - path: PathBuf::from("tests/fixture/state/pending_exercise.rs"), - mode: *mode, - hint: String::from(""), - }; - let _ = exercise.compile().unwrap(); - assert!(!Path::new(&format!("{}.pdb", temp_file())).exists()); - }); + #[inline] + fn strict_clippy(&self) -> bool { + self.strict_clippy } - #[test] - fn test_pending_state() { - let exercise = Exercise { - name: "pending_exercise".into(), - path: PathBuf::from("tests/fixture/state/pending_exercise.rs"), - mode: Mode::Compile, - hint: String::new(), - }; - - let state = exercise.state(); - let expected = vec![ - ContextLine { - line: "// fake_exercise".to_string(), - number: 1, - important: false, - }, - ContextLine { - line: "".to_string(), - number: 2, - important: false, - }, - ContextLine { - line: "// I AM NOT DONE".to_string(), - number: 3, - important: true, - }, - ContextLine { - line: "".to_string(), - number: 4, - important: false, - }, - ContextLine { - line: "fn main() {".to_string(), - number: 5, - important: false, - }, - ]; - - assert_eq!(state, State::Pending(expected)); - } - - #[test] - fn test_finished_exercise() { - let exercise = Exercise { - name: "finished_exercise".into(), - path: PathBuf::from("tests/fixture/state/finished_exercise.rs"), - mode: Mode::Compile, - hint: String::new(), - }; - - assert_eq!(exercise.state(), State::Done); - } - - #[test] - fn test_exercise_with_output() { - let exercise = Exercise { - name: "exercise_with_output".into(), - path: PathBuf::from("tests/fixture/success/testSuccess.rs"), - mode: Mode::Test, - hint: String::new(), - }; - let out = exercise.compile().unwrap().run().unwrap(); - assert!(out.stdout.contains("THIS TEST TOO SHALL PASS")); - } - - #[test] - fn test_not_done() { - assert!(contains_not_done_comment("// I AM NOT DONE")); - assert!(contains_not_done_comment("/// I AM NOT DONE")); - assert!(contains_not_done_comment("// I AM NOT DONE")); - assert!(contains_not_done_comment("/// I AM NOT DONE")); - assert!(contains_not_done_comment("// I AM NOT DONE ")); - assert!(contains_not_done_comment("// I AM NOT DONE!")); - assert!(contains_not_done_comment("// I am not done")); - assert!(contains_not_done_comment("// i am NOT done")); - - assert!(!contains_not_done_comment("I AM NOT DONE")); - assert!(!contains_not_done_comment("// NOT DONE")); - assert!(!contains_not_done_comment("DONE")); + #[inline] + fn test(&self) -> bool { + self.test } } diff --git a/src/info_file.rs b/src/info_file.rs new file mode 100644 index 00000000..f226f735 --- /dev/null +++ b/src/info_file.rs @@ -0,0 +1,135 @@ +use anyhow::{bail, Context, Error, Result}; +use serde::Deserialize; +use std::{fs, io::ErrorKind}; + +use crate::{embedded::EMBEDDED_FILES, exercise::RunnableExercise}; + +/// Deserialized from the `info.toml` file. +#[derive(Deserialize)] +pub struct ExerciseInfo { + /// Exercise's unique name. + pub name: String, + /// Exercise's directory name inside the `exercises/` directory. + pub dir: Option, + #[serde(default = "default_true")] + /// Run `cargo test` on the exercise. + pub test: bool, + /// Deny all Clippy warnings. + #[serde(default)] + pub strict_clippy: bool, + /// The exercise's hint to be shown to the user on request. + pub hint: String, +} +#[inline(always)] +const fn default_true() -> bool { + true +} + +impl ExerciseInfo { + /// Path to the exercise file starting with the `exercises/` directory. + pub fn path(&self) -> String { + let mut path = if let Some(dir) = &self.dir { + // 14 = 10 + 1 + 3 + // exercises/ + / + .rs + let mut path = String::with_capacity(14 + dir.len() + self.name.len()); + path.push_str("exercises/"); + path.push_str(dir); + path.push('/'); + path + } else { + // 13 = 10 + 3 + // exercises/ + .rs + let mut path = String::with_capacity(13 + self.name.len()); + path.push_str("exercises/"); + path + }; + + path.push_str(&self.name); + path.push_str(".rs"); + + path + } + + /// Path to the solution file starting with the `solutions/` directory. + pub fn sol_path(&self) -> String { + let mut path = if let Some(dir) = &self.dir { + // 14 = 10 + 1 + 3 + // solutions/ + / + .rs + let mut path = String::with_capacity(14 + dir.len() + self.name.len()); + path.push_str("solutions/"); + path.push_str(dir); + path.push('/'); + path + } else { + // 13 = 10 + 3 + // solutions/ + .rs + let mut path = String::with_capacity(13 + self.name.len()); + path.push_str("solutions/"); + path + }; + + path.push_str(&self.name); + path.push_str(".rs"); + + path + } +} + +impl RunnableExercise for ExerciseInfo { + #[inline] + fn name(&self) -> &str { + &self.name + } + + #[inline] + fn strict_clippy(&self) -> bool { + self.strict_clippy + } + + #[inline] + fn test(&self) -> bool { + self.test + } +} + +/// The deserialized `info.toml` file. +#[derive(Deserialize)] +pub struct InfoFile { + /// For possible breaking changes in the future for third-party exercises. + pub format_version: u8, + /// Shown to users when starting with the exercises. + pub welcome_message: Option, + /// Shown to users after finishing all exercises. + pub final_message: Option, + /// List of all exercises. + pub exercises: Vec, +} + +impl InfoFile { + /// Official exercises: Parse the embedded `info.toml` file. + /// Third-party exercises: Parse the `info.toml` file in the current directory. + pub fn parse() -> Result { + // Read a local `info.toml` if it exists. + let slf = match fs::read_to_string("info.toml") { + Ok(file_content) => toml_edit::de::from_str::(&file_content) + .context("Failed to parse the `info.toml` file")?, + Err(e) => { + if e.kind() == ErrorKind::NotFound { + return toml_edit::de::from_str(EMBEDDED_FILES.info_file) + .context("Failed to parse the embedded `info.toml` file"); + } + + return Err(Error::from(e).context("Failed to read the `info.toml` file")); + } + }; + + if slf.exercises.is_empty() { + bail!("{NO_EXERCISES_ERR}"); + } + + Ok(slf) + } +} + +const NO_EXERCISES_ERR: &str = "There are no exercises yet! +If you are developing third-party exercises, add at least one exercise before testing."; diff --git a/src/init.rs b/src/init.rs new file mode 100644 index 00000000..4063ca75 --- /dev/null +++ b/src/init.rs @@ -0,0 +1,116 @@ +use anyhow::{bail, Context, Result}; +use crossterm::style::Stylize; +use std::{ + env::set_current_dir, + fs::{self, create_dir}, + io::ErrorKind, + path::Path, + process::{Command, Stdio}, +}; + +use crate::{cargo_toml::updated_cargo_toml, embedded::EMBEDDED_FILES, info_file::InfoFile}; + +pub fn init() -> Result<()> { + // Prevent initialization in a directory that contains the file `Cargo.toml`. + // This can mean that Rustlings was already initialized in this directory. + // Otherwise, this can cause problems with Cargo workspaces. + if Path::new("Cargo.toml").exists() { + bail!(CARGO_TOML_EXISTS_ERR); + } + + let rustlings_path = Path::new("rustlings"); + if let Err(e) = create_dir(rustlings_path) { + if e.kind() == ErrorKind::AlreadyExists { + bail!(RUSTLINGS_DIR_ALREADY_EXISTS_ERR); + } + return Err(e.into()); + } + + set_current_dir("rustlings") + .context("Failed to change the current directory to `rustlings/`")?; + + let info_file = InfoFile::parse()?; + EMBEDDED_FILES + .init_exercises_dir(&info_file.exercises) + .context("Failed to initialize the `rustlings/exercises` directory")?; + + create_dir("solutions").context("Failed to create the `solutions/` directory")?; + for dir in EMBEDDED_FILES.exercise_dirs { + let mut dir_path = String::with_capacity(10 + dir.name.len()); + dir_path.push_str("solutions/"); + dir_path.push_str(dir.name); + create_dir(&dir_path) + .with_context(|| format!("Failed to create the directory {dir_path}"))?; + } + for exercise_info in &info_file.exercises { + let solution_path = exercise_info.sol_path(); + fs::write(&solution_path, INIT_SOLUTION_FILE) + .with_context(|| format!("Failed to create the file {solution_path}"))?; + } + + let current_cargo_toml = include_str!("../dev-Cargo.toml"); + // Skip the first line (comment). + let newline_ind = current_cargo_toml + .as_bytes() + .iter() + .position(|c| *c == b'\n') + .context("The embedded `Cargo.toml` is empty or contains only one line")?; + let current_cargo_toml = current_cargo_toml + .get(newline_ind + 1..) + .context("The embedded `Cargo.toml` contains only one line")?; + let updated_cargo_toml = updated_cargo_toml(&info_file.exercises, current_cargo_toml, b"") + .context("Failed to generate `Cargo.toml`")?; + fs::write("Cargo.toml", updated_cargo_toml) + .context("Failed to create the file `rustlings/Cargo.toml`")?; + + fs::write(".gitignore", GITIGNORE) + .context("Failed to create the file `rustlings/.gitignore`")?; + + create_dir(".vscode").context("Failed to create the directory `rustlings/.vscode`")?; + fs::write(".vscode/extensions.json", VS_CODE_EXTENSIONS_JSON) + .context("Failed to create the file `rustlings/.vscode/extensions.json`")?; + + // Ignore any Git error because Git initialization is not required. + let _ = Command::new("git") + .arg("init") + .stdin(Stdio::null()) + .stderr(Stdio::null()) + .status(); + + println!( + "\n{}\n\n{}", + "Initialization done ✓".green(), + POST_INIT_MSG.bold(), + ); + + Ok(()) +} + +const INIT_SOLUTION_FILE: &[u8] = b"fn main() { + // DON'T EDIT THIS SOLUTION FILE! + // It will be automatically filled after you finish the exercise. +} +"; + +const GITIGNORE: &[u8] = b".rustlings-state.txt +solutions +Cargo.lock +target +.vscode +"; + +pub const VS_CODE_EXTENSIONS_JSON: &[u8] = br#"{"recommendations":["rust-lang.rust-analyzer"]}"#; + +const CARGO_TOML_EXISTS_ERR: &str = "The current directory contains the file `Cargo.toml`. + +If you already initialized Rustlings, run the command `rustlings` for instructions on getting started with the exercises. +Otherwise, please run `rustlings init` again in another directory."; + +const RUSTLINGS_DIR_ALREADY_EXISTS_ERR: &str = + "A directory with the name `rustlings` already exists in the current directory. +You probably already initialized Rustlings. +Run `cd rustlings` +Then run `rustlings` again"; + +const POST_INIT_MSG: &str = "Run `cd rustlings` to go into the generated directory. +Then run `rustlings` to get started."; diff --git a/src/list.rs b/src/list.rs new file mode 100644 index 00000000..790c02fe --- /dev/null +++ b/src/list.rs @@ -0,0 +1,90 @@ +use anyhow::Result; +use crossterm::{ + event::{self, Event, KeyCode, KeyEventKind}, + terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, + ExecutableCommand, +}; +use ratatui::{backend::CrosstermBackend, Terminal}; +use std::io; + +use crate::app_state::AppState; + +use self::state::{Filter, UiState}; + +mod state; + +pub fn list(app_state: &mut AppState) -> Result<()> { + let mut stdout = io::stdout().lock(); + stdout.execute(EnterAlternateScreen)?; + enable_raw_mode()?; + + let mut terminal = Terminal::new(CrosstermBackend::new(&mut stdout))?; + terminal.clear()?; + + let mut ui_state = UiState::new(app_state); + + 'outer: loop { + terminal.draw(|frame| ui_state.draw(frame).unwrap())?; + + let key = loop { + match event::read()? { + Event::Key(key) => match key.kind { + KeyEventKind::Press | KeyEventKind::Repeat => break key, + KeyEventKind::Release => (), + }, + // Redraw + Event::Resize(_, _) => continue 'outer, + // Ignore + Event::FocusGained | Event::FocusLost | Event::Mouse(_) | Event::Paste(_) => (), + } + }; + + ui_state.message.clear(); + + match key.code { + KeyCode::Char('q') => break, + KeyCode::Down | KeyCode::Char('j') => ui_state.select_next(), + KeyCode::Up | KeyCode::Char('k') => ui_state.select_previous(), + KeyCode::Home | KeyCode::Char('g') => ui_state.select_first(), + KeyCode::End | KeyCode::Char('G') => ui_state.select_last(), + KeyCode::Char('d') => { + let message = if ui_state.filter == Filter::Done { + ui_state.filter = Filter::None; + "Disabled filter DONE" + } else { + ui_state.filter = Filter::Done; + "Enabled filter DONE │ Press d again to disable the filter" + }; + + ui_state = ui_state.with_updated_rows(); + ui_state.message.push_str(message); + } + KeyCode::Char('p') => { + let message = if ui_state.filter == Filter::Pending { + ui_state.filter = Filter::None; + "Disabled filter PENDING" + } else { + ui_state.filter = Filter::Pending; + "Enabled filter PENDING │ Press p again to disable the filter" + }; + + ui_state = ui_state.with_updated_rows(); + ui_state.message.push_str(message); + } + KeyCode::Char('r') => { + ui_state = ui_state.with_reset_selected()?; + } + KeyCode::Char('c') => { + ui_state.selected_to_current_exercise()?; + ui_state = ui_state.with_updated_rows(); + } + _ => (), + } + } + + drop(terminal); + stdout.execute(LeaveAlternateScreen)?; + disable_raw_mode()?; + + Ok(()) +} diff --git a/src/list/state.rs b/src/list/state.rs new file mode 100644 index 00000000..d6df6344 --- /dev/null +++ b/src/list/state.rs @@ -0,0 +1,271 @@ +use anyhow::{Context, Result}; +use ratatui::{ + layout::{Constraint, Rect}, + style::{Style, Stylize}, + text::{Line, Span}, + widgets::{Block, Borders, HighlightSpacing, Paragraph, Row, Table, TableState}, + Frame, +}; +use std::fmt::Write; + +use crate::{app_state::AppState, progress_bar::progress_bar_ratatui}; + +#[derive(Copy, Clone, PartialEq, Eq)] +pub enum Filter { + Done, + Pending, + None, +} + +pub struct UiState<'a> { + pub table: Table<'static>, + pub message: String, + pub filter: Filter, + app_state: &'a mut AppState, + table_state: TableState, + n_rows: usize, +} + +impl<'a> UiState<'a> { + pub fn with_updated_rows(mut self) -> Self { + let current_exercise_ind = self.app_state.current_exercise_ind(); + + self.n_rows = 0; + let rows = self + .app_state + .exercises() + .iter() + .enumerate() + .filter_map(|(ind, exercise)| { + let exercise_state = if exercise.done { + if self.filter == Filter::Pending { + return None; + } + + "DONE".green() + } else { + if self.filter == Filter::Done { + return None; + } + + "PENDING".yellow() + }; + + self.n_rows += 1; + + let next = if ind == current_exercise_ind { + ">>>>".bold().red() + } else { + Span::default() + }; + + Some(Row::new([ + next, + exercise_state, + Span::raw(exercise.name), + Span::raw(exercise.path), + ])) + }); + + self.table = self.table.rows(rows); + + if self.n_rows == 0 { + self.table_state.select(None); + } else { + self.table_state.select(Some( + self.table_state + .selected() + .map_or(0, |selected| selected.min(self.n_rows - 1)), + )); + } + + self + } + + pub fn new(app_state: &'a mut AppState) -> Self { + let header = Row::new(["Next", "State", "Name", "Path"]); + + let max_name_len = app_state + .exercises() + .iter() + .map(|exercise| exercise.name.len()) + .max() + .unwrap_or(4) as u16; + + let widths = [ + Constraint::Length(4), + Constraint::Length(7), + Constraint::Length(max_name_len), + Constraint::Fill(1), + ]; + + let table = Table::default() + .widths(widths) + .header(header) + .column_spacing(2) + .highlight_spacing(HighlightSpacing::Always) + .highlight_style(Style::new().bg(ratatui::style::Color::Rgb(50, 50, 50))) + .highlight_symbol("🦀") + .block(Block::default().borders(Borders::BOTTOM)); + + let selected = app_state.current_exercise_ind(); + let table_state = TableState::default() + .with_offset(selected.saturating_sub(10)) + .with_selected(Some(selected)); + + let filter = Filter::None; + let n_rows = app_state.exercises().len(); + + let slf = Self { + table, + message: String::with_capacity(128), + filter, + app_state, + table_state, + n_rows, + }; + + slf.with_updated_rows() + } + + pub fn select_next(&mut self) { + if self.n_rows > 0 { + let next = self + .table_state + .selected() + .map_or(0, |selected| (selected + 1).min(self.n_rows - 1)); + self.table_state.select(Some(next)); + } + } + + pub fn select_previous(&mut self) { + if self.n_rows > 0 { + let previous = self + .table_state + .selected() + .map_or(0, |selected| selected.saturating_sub(1)); + self.table_state.select(Some(previous)); + } + } + + pub fn select_first(&mut self) { + if self.n_rows > 0 { + self.table_state.select(Some(0)); + } + } + + pub fn select_last(&mut self) { + if self.n_rows > 0 { + self.table_state.select(Some(self.n_rows - 1)); + } + } + + pub fn draw(&mut self, frame: &mut Frame) -> Result<()> { + let area = frame.size(); + + frame.render_stateful_widget( + &self.table, + Rect { + x: 0, + y: 0, + width: area.width, + height: area.height - 3, + }, + &mut self.table_state, + ); + + frame.render_widget( + Paragraph::new(progress_bar_ratatui( + self.app_state.n_done(), + self.app_state.exercises().len() as u16, + area.width, + )?) + .block(Block::default().borders(Borders::BOTTOM)), + Rect { + x: 0, + y: area.height - 3, + width: area.width, + height: 2, + }, + ); + + let message = if self.message.is_empty() { + // Help footer. + let mut spans = Vec::with_capacity(4); + spans.push(Span::raw( + "↓/j ↑/k home/g end/G │ ontinue at │ eset │ filter ", + )); + match self.filter { + Filter::Done => { + spans.push("one".underlined().magenta()); + spans.push(Span::raw("/

ending")); + } + Filter::Pending => { + spans.push(Span::raw("one/")); + spans.push("

ending".underlined().magenta()); + } + Filter::None => spans.push(Span::raw("one/

ending")), + } + spans.push(Span::raw(" │ uit")); + Line::from(spans) + } else { + Line::from(self.message.as_str().light_blue()) + }; + frame.render_widget( + message, + Rect { + x: 0, + y: area.height - 1, + width: area.width, + height: 1, + }, + ); + + Ok(()) + } + + pub fn with_reset_selected(mut self) -> Result { + let Some(selected) = self.table_state.selected() else { + return Ok(self); + }; + + let ind = self + .app_state + .exercises() + .iter() + .enumerate() + .filter_map(|(ind, exercise)| match self.filter { + Filter::Done => exercise.done.then_some(ind), + Filter::Pending => (!exercise.done).then_some(ind), + Filter::None => Some(ind), + }) + .nth(selected) + .context("Invalid selection index")?; + + let exercise_path = self.app_state.reset_exercise_by_ind(ind)?; + write!(self.message, "The exercise {exercise_path} has been reset")?; + + Ok(self.with_updated_rows()) + } + + pub fn selected_to_current_exercise(&mut self) -> Result<()> { + let Some(selected) = self.table_state.selected() else { + return Ok(()); + }; + + let ind = self + .app_state + .exercises() + .iter() + .enumerate() + .filter_map(|(ind, exercise)| match self.filter { + Filter::Done => exercise.done.then_some(ind), + Filter::Pending => (!exercise.done).then_some(ind), + Filter::None => Some(ind), + }) + .nth(selected) + .context("Invalid selection index")?; + + self.app_state.set_current_exercise_ind(ind) + } +} diff --git a/src/main.rs b/src/main.rs index 8f73dbba..2233d8b0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,483 +1,220 @@ -use crate::exercise::{Exercise, ExerciseList}; -use crate::project::write_project_json; -use crate::run::{reset, run}; -use crate::verify::verify; -use anyhow::Result; +use anyhow::{bail, Context, Result}; +use app_state::StateFileStatus; use clap::{Parser, Subcommand}; -use console::Emoji; -use notify_debouncer_mini::notify::{self, RecursiveMode}; -use notify_debouncer_mini::{new_debouncer, DebouncedEventKind}; -use shlex::Shlex; -use std::ffi::OsStr; -use std::fs; -use std::io::{self, prelude::*}; -use std::path::Path; -use std::process::Command; -use std::sync::atomic::{AtomicBool, Ordering}; -use std::sync::mpsc::{channel, RecvTimeoutError}; -use std::sync::{Arc, Mutex}; -use std::thread; -use std::time::Duration; +use std::{ + io::{self, BufRead, StdoutLock, Write}, + path::Path, + process::exit, +}; -#[macro_use] -mod ui; +use self::{app_state::AppState, dev::DevCommands, info_file::InfoFile, watch::WatchExit}; +mod app_state; +mod cargo_toml; +mod cmd; +mod dev; +mod embedded; mod exercise; -mod project; +mod info_file; +mod init; +mod list; +mod progress_bar; mod run; -mod verify; +mod terminal_link; +mod watch; + +const CURRENT_FORMAT_VERSION: u8 = 1; +const DEBUG_PROFILE: bool = { + #[allow(unused_assignments, unused_mut)] + let mut debug_profile = false; + + #[cfg(debug_assertions)] + { + debug_profile = true; + } + + debug_profile +}; + +// The current directory is the official Rustligns repository. +fn in_official_repo() -> bool { + Path::new("dev/rustlings-repo.txt").exists() +} + +fn clear_terminal(stdout: &mut StdoutLock) -> io::Result<()> { + stdout.write_all(b"\x1b[H\x1b[2J\x1b[3J") +} + +fn press_enter_prompt() -> io::Result<()> { + io::stdin().lock().read_until(b'\n', &mut Vec::new())?; + Ok(()) +} /// Rustlings is a collection of small exercises to get you used to writing and reading Rust code #[derive(Parser)] #[command(version)] struct Args { - /// Show outputs from the test exercises - #[arg(long)] - nocapture: bool, #[command(subcommand)] command: Option, + /// Manually run the current exercise using `r` in the watch mode. + /// Only use this if Rustlings fails to detect exercise file changes. + #[arg(long)] + manual_run: bool, } #[derive(Subcommand)] enum Subcommands { - /// Verify all exercises according to the recommended order - Verify, - /// Rerun `verify` when files were edited - Watch { - /// Show hints on success - #[arg(long)] - success_hints: bool, - }, - /// Run/Test a single exercise + /// Initialize the official Rustlings exercises + Init, + /// Run a single exercise. Runs the next pending exercise if the exercise name is not specified Run { /// The name of the exercise - name: String, + name: Option, }, - /// Reset a single exercise using "git stash -- " + /// Reset a single exercise Reset { /// The name of the exercise name: String, }, - /// Return a hint for the given exercise + /// Show a hint. Shows the hint of the next pending exercise if the exercise name is not specified Hint { /// The name of the exercise - name: String, + name: Option, }, - /// List the exercises available in Rustlings - List { - /// Show only the paths of the exercises - #[arg(short, long)] - paths: bool, - /// Show only the names of the exercises - #[arg(short, long)] - names: bool, - /// Provide a string to match exercise names. - /// Comma separated patterns are accepted - #[arg(short, long)] - filter: Option, - /// Display only exercises not yet solved - #[arg(short, long)] - unsolved: bool, - /// Display only exercises that have been solved - #[arg(short, long)] - solved: bool, - }, - /// Enable rust-analyzer for exercises - Lsp, + /// Commands for developing (third-party) Rustlings exercises + #[command(subcommand)] + Dev(DevCommands), } fn main() -> Result<()> { let args = Args::parse(); - if args.command.is_none() { - println!("\n{WELCOME}\n"); + if !DEBUG_PROFILE && in_official_repo() { + bail!("{OLD_METHOD_ERR}"); } - if which::which("rustc").is_err() { - println!("We cannot find `rustc`."); - println!("Try running `rustc --version` to diagnose your problem."); - println!("For instructions on how to install Rust, check the README."); - std::process::exit(1); - } - - let info_file = fs::read_to_string("info.toml").unwrap_or_else(|e| { - match e.kind() { - io::ErrorKind::NotFound => println!( - "The program must be run from the rustlings directory\nTry `cd rustlings/`!", - ), - _ => println!("Failed to read the info.toml file: {e}"), - } - std::process::exit(1); - }); - let exercises = toml_edit::de::from_str::(&info_file) - .unwrap() - .exercises; - let verbose = args.nocapture; - - let command = args.command.unwrap_or_else(|| { - println!("{DEFAULT_OUT}\n"); - std::process::exit(0); - }); - - match command { - Subcommands::List { - paths, - names, - filter, - unsolved, - solved, - } => { - if !paths && !names { - println!("{:<17}\t{:<46}\t{:<7}", "Name", "Path", "Status"); + match args.command { + Some(Subcommands::Init) => { + if DEBUG_PROFILE { + bail!("Disabled in the debug build"); } - let mut exercises_done: u16 = 0; - let lowercase_filter = filter - .as_ref() - .map(|s| s.to_lowercase()) - .unwrap_or_default(); - let filters = lowercase_filter - .split(',') - .filter_map(|f| { - let f = f.trim(); - if f.is_empty() { - None - } else { - Some(f) - } - }) - .collect::>(); - for exercise in &exercises { - let fname = exercise.path.to_string_lossy(); - let filter_cond = filters - .iter() - .any(|f| exercise.name.contains(f) || fname.contains(f)); - let looks_done = exercise.looks_done(); - let status = if looks_done { - exercises_done += 1; - "Done" - } else { - "Pending" - }; - let solve_cond = - (looks_done && solved) || (!looks_done && unsolved) || (!solved && !unsolved); - if solve_cond && (filter_cond || filter.is_none()) { - let line = if paths { - format!("{fname}\n") - } else if names { - format!("{}\n", exercise.name) - } else { - format!("{:<17}\t{fname:<46}\t{status:<7}\n", exercise.name) - }; - // Somehow using println! leads to the binary panicking - // when its output is piped. - // So, we're handling a Broken Pipe error and exiting with 0 anyway - let stdout = std::io::stdout(); - { - let mut handle = stdout.lock(); - handle.write_all(line.as_bytes()).unwrap_or_else(|e| { - match e.kind() { - std::io::ErrorKind::BrokenPipe => std::process::exit(0), - _ => std::process::exit(1), - }; - }); - } + { + let mut stdout = io::stdout().lock(); + stdout.write_all(b"This command will create the directory `rustlings/` which will contain the exercises.\nPress ENTER to continue ")?; + stdout.flush()?; + press_enter_prompt()?; + stdout.write_all(b"\n")?; + } + + return init::init().context("Initialization failed"); + } + Some(Subcommands::Dev(dev_command)) => return dev_command.run(), + _ => (), + } + + if !Path::new("exercises").is_dir() { + println!("{PRE_INIT_MSG}"); + exit(1); + } + + let info_file = InfoFile::parse()?; + + if info_file.format_version > CURRENT_FORMAT_VERSION { + bail!(FORMAT_VERSION_HIGHER_ERR); + } + + let (mut app_state, state_file_status) = AppState::new( + info_file.exercises, + info_file.final_message.unwrap_or_default(), + )?; + + // Show the welcome message if the state file doesn't exist yet. + if let Some(welcome_message) = info_file.welcome_message { + match state_file_status { + StateFileStatus::NotRead => { + let mut stdout = io::stdout().lock(); + clear_terminal(&mut stdout)?; + + let welcome_message = welcome_message.trim(); + write!(stdout, "{welcome_message}\n\nPress ENTER to continue ")?; + stdout.flush()?; + press_enter_prompt()?; + clear_terminal(&mut stdout)?; + } + StateFileStatus::Read => (), + } + } + + match args.command { + None => { + let notify_exercise_names = if args.manual_run { + None + } else { + // For the notify event handler thread. + // Leaking is not a problem because the slice lives until the end of the program. + Some( + &*app_state + .exercises() + .iter() + .map(|exercise| exercise.name.as_bytes()) + .collect::>() + .leak(), + ) + }; + + loop { + match watch::watch(&mut app_state, notify_exercise_names)? { + WatchExit::Shutdown => break, + // It is much easier to exit the watch mode, launch the list mode and then restart + // the watch mode instead of trying to pause the watch threads and correct the + // watch state. + WatchExit::List => list::list(&mut app_state)?, } } - - let percentage_progress = exercises_done as f32 / exercises.len() as f32 * 100.0; - println!( - "Progress: You completed {} / {} exercises ({:.1} %).", - exercises_done, - exercises.len(), - percentage_progress - ); - std::process::exit(0); } - - Subcommands::Run { name } => { - let exercise = find_exercise(&name, &exercises); - - run(exercise, verbose).unwrap_or_else(|_| std::process::exit(1)); - } - - Subcommands::Reset { name } => { - let exercise = find_exercise(&name, &exercises); - - reset(exercise).unwrap_or_else(|_| std::process::exit(1)); - } - - Subcommands::Hint { name } => { - let exercise = find_exercise(&name, &exercises); - - println!("{}", exercise.hint); - } - - Subcommands::Verify => { - verify(&exercises, (0, exercises.len()), verbose, false) - .unwrap_or_else(|_| std::process::exit(1)); - } - - Subcommands::Lsp => { - if let Err(e) = write_project_json(exercises) { - println!("Failed to write rust-project.json to disk for rust-analyzer: {e}"); - } else { - println!("Successfully generated rust-project.json"); - println!("rust-analyzer will now parse exercises, restart your language server or editor"); + Some(Subcommands::Run { name }) => { + if let Some(name) = name { + app_state.set_current_exercise_by_name(&name)?; } + run::run(&mut app_state)?; } - - Subcommands::Watch { success_hints } => match watch(&exercises, verbose, success_hints) { - Err(e) => { - println!("Error: Could not watch your progress. Error message was {e:?}."); - println!("Most likely you've run out of disk space or your 'inotify limit' has been reached."); - std::process::exit(1); + Some(Subcommands::Reset { name }) => { + app_state.set_current_exercise_by_name(&name)?; + let exercise_path = app_state.reset_current_exercise()?; + println!("The exercise {exercise_path} has been reset"); + } + Some(Subcommands::Hint { name }) => { + if let Some(name) = name { + app_state.set_current_exercise_by_name(&name)?; } - Ok(WatchStatus::Finished) => { - println!( - "{emoji} All exercises completed! {emoji}", - emoji = Emoji("🎉", "★") - ); - println!("\n{FENISH_LINE}\n"); - } - Ok(WatchStatus::Unfinished) => { - println!("We hope you're enjoying learning about Rust!"); - println!("If you want to continue working on the exercises at a later point, you can simply run `rustlings watch` again"); - } - }, + println!("{}", app_state.current_exercise().hint); + } + // Handled in an earlier match. + Some(Subcommands::Init | Subcommands::Dev(_)) => (), } Ok(()) } -fn spawn_watch_shell( - failed_exercise_hint: Arc>>, - should_quit: Arc, -) { - println!("Welcome to watch mode! You can type 'help' to get an overview of the commands you can use here."); +const OLD_METHOD_ERR: &str = "You are trying to run Rustlings using the old method before v6. +The new method doesn't include cloning the Rustlings' repository. +Please follow the instructions in the README: +https://github.com/rust-lang/rustlings#getting-started"; - thread::spawn(move || { - let mut input = String::with_capacity(32); - let mut stdin = io::stdin().lock(); +const FORMAT_VERSION_HIGHER_ERR: &str = + "The format version specified in the `info.toml` file is higher than the last one supported. +It is possible that you have an outdated version of Rustlings. +Try to install the latest Rustlings version first."; - loop { - // Recycle input buffer. - input.clear(); - - if let Err(e) = stdin.read_line(&mut input) { - println!("error reading command: {e}"); - } - - let input = input.trim(); - if input == "hint" { - if let Some(hint) = &*failed_exercise_hint.lock().unwrap() { - println!("{hint}"); - } - } else if input == "clear" { - println!("\x1B[2J\x1B[1;1H"); - } else if input == "quit" { - should_quit.store(true, Ordering::SeqCst); - println!("Bye!"); - } else if input == "help" { - println!("{WATCH_MODE_HELP_MESSAGE}"); - } else if let Some(cmd) = input.strip_prefix('!') { - let mut parts = Shlex::new(cmd); - - let Some(program) = parts.next() else { - println!("no command provided"); - continue; - }; - - if let Err(e) = Command::new(program).args(parts).status() { - println!("failed to execute command `{cmd}`: {e}"); - } - } else { - println!("unknown command: {input}\n{WATCH_MODE_HELP_MESSAGE}"); - } - } - }); -} - -fn find_exercise<'a>(name: &str, exercises: &'a [Exercise]) -> &'a Exercise { - if name == "next" { - exercises - .iter() - .find(|e| !e.looks_done()) - .unwrap_or_else(|| { - println!("🎉 Congratulations! You have done all the exercises!"); - println!("🔚 There are no more exercises to do next!"); - std::process::exit(1) - }) - } else { - exercises - .iter() - .find(|e| e.name == name) - .unwrap_or_else(|| { - println!("No exercise found for '{name}'!"); - std::process::exit(1) - }) - } -} - -enum WatchStatus { - Finished, - Unfinished, -} - -fn watch( - exercises: &[Exercise], - verbose: bool, - success_hints: bool, -) -> notify::Result { - /* Clears the terminal with an ANSI escape code. - Works in UNIX and newer Windows terminals. */ - fn clear_screen() { - println!("\x1Bc"); - } - - let (tx, rx) = channel(); - let should_quit = Arc::new(AtomicBool::new(false)); - - let mut debouncer = new_debouncer(Duration::from_secs(1), tx)?; - debouncer - .watcher() - .watch(Path::new("./exercises"), RecursiveMode::Recursive)?; - - clear_screen(); - - let failed_exercise_hint = match verify( - exercises.iter(), - (0, exercises.len()), - verbose, - success_hints, - ) { - Ok(_) => return Ok(WatchStatus::Finished), - Err(exercise) => Arc::new(Mutex::new(Some(exercise.hint.clone()))), - }; - spawn_watch_shell(Arc::clone(&failed_exercise_hint), Arc::clone(&should_quit)); - loop { - match rx.recv_timeout(Duration::from_secs(1)) { - Ok(event) => match event { - Ok(events) => { - for event in events { - let event_path = event.path; - if event.kind == DebouncedEventKind::Any - && event_path.extension() == Some(OsStr::new("rs")) - && event_path.exists() - { - let filepath = event_path.as_path().canonicalize().unwrap(); - let pending_exercises = - exercises - .iter() - .find(|e| filepath.ends_with(&e.path)) - .into_iter() - .chain(exercises.iter().filter(|e| { - !e.looks_done() && !filepath.ends_with(&e.path) - })); - let num_done = exercises - .iter() - .filter(|e| e.looks_done() && !filepath.ends_with(&e.path)) - .count(); - clear_screen(); - match verify( - pending_exercises, - (num_done, exercises.len()), - verbose, - success_hints, - ) { - Ok(_) => return Ok(WatchStatus::Finished), - Err(exercise) => { - let mut failed_exercise_hint = - failed_exercise_hint.lock().unwrap(); - *failed_exercise_hint = Some(exercise.hint.clone()); - } - } - } - } - } - Err(e) => println!("watch error: {e:?}"), - }, - Err(RecvTimeoutError::Timeout) => { - // the timeout expired, just check the `should_quit` variable below then loop again - } - Err(e) => println!("watch error: {e:?}"), - } - // Check if we need to exit - if should_quit.load(Ordering::SeqCst) { - return Ok(WatchStatus::Unfinished); - } - } -} - -const DEFAULT_OUT: &str = "Thanks for installing Rustlings! - -Is this your first time? Don't worry, Rustlings was made for beginners! We are -going to teach you a lot of things about Rust, but before we can get -started, here's a couple of notes about how Rustlings operates: - -1. The central concept behind Rustlings is that you solve exercises. These - exercises usually have some sort of syntax error in them, which will cause - them to fail compilation or testing. Sometimes there's a logic error instead - of a syntax error. No matter what error, it's your job to find it and fix it! - You'll know when you fixed it because then, the exercise will compile and - Rustlings will be able to move on to the next exercise. -2. If you run Rustlings in watch mode (which we recommend), it'll automatically - start with the first exercise. Don't get confused by an error message popping - up as soon as you run Rustlings! This is part of the exercise that you're - supposed to solve, so open the exercise file in an editor and start your - detective work! -3. If you're stuck on an exercise, there is a helpful hint you can view by typing - 'hint' (in watch mode), or running `rustlings hint exercise_name`. -4. If an exercise doesn't make sense to you, feel free to open an issue on GitHub! - (https://github.com/rust-lang/rustlings/issues/new). We look at every issue, - and sometimes, other learners do too so you can help each other out! -5. If you want to use `rust-analyzer` with exercises, which provides features like - autocompletion, run the command `rustlings lsp`. - -Got all that? Great! To get started, run `rustlings watch` in order to get the first -exercise. Make sure to have your editor open!"; - -const FENISH_LINE: &str = "+----------------------------------------------------+ -| You made it to the Fe-nish line! | -+-------------------------- ------------------------+ - \\/\x1b[31m - ▒▒ ▒▒▒▒▒▒▒▒ ▒▒▒▒▒▒▒▒ ▒▒ - ▒▒▒▒ ▒▒ ▒▒ ▒▒ ▒▒ ▒▒ ▒▒ ▒▒▒▒ - ▒▒▒▒ ▒▒ ▒▒ ▒▒ ▒▒ ▒▒ ▒▒▒▒ - ░░▒▒▒▒░░▒▒ ▒▒ ▒▒ ▒▒ ▒▒░░▒▒▒▒ - ▓▓▓▓▓▓▓▓ ▓▓ ▓▓██ ▓▓ ▓▓██ ▓▓ ▓▓▓▓▓▓▓▓ - ▒▒▒▒ ▒▒ ████ ▒▒ ████ ▒▒░░ ▒▒▒▒ - ▒▒ ▒▒▒▒▒▒ ▒▒▒▒▒▒ ▒▒▒▒▒▒ ▒▒ - ▒▒▒▒▒▒▒▒▒▒▓▓▓▓▓▓▒▒▒▒▒▒▒▒▓▓▒▒▓▓▒▒▒▒▒▒▒▒ - ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ - ▒▒▒▒▒▒▒▒▒▒██▒▒▒▒▒▒██▒▒▒▒▒▒▒▒▒▒ - ▒▒ ▒▒▒▒▒▒▒▒▒▒██████▒▒▒▒▒▒▒▒▒▒ ▒▒ - ▒▒ ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ ▒▒ - ▒▒ ▒▒ ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ ▒▒ ▒▒ - ▒▒ ▒▒ ▒▒ ▒▒ ▒▒ ▒▒ - ▒▒ ▒▒ ▒▒ ▒▒\x1b[0m - -We hope you enjoyed learning about the various aspects of Rust! -If you noticed any issues, please don't hesitate to report them to our repo. -You can also contribute your own exercises to help the greater community! - -Before reporting an issue or contributing, please read our guidelines: -https://github.com/rust-lang/rustlings/blob/main/CONTRIBUTING.md"; - -const WELCOME: &str = r" welcome to... +const PRE_INIT_MSG: &str = r" + Welcome to... _ _ _ _ __ _ _ ___| |_| (_)_ __ __ _ ___ | '__| | | / __| __| | | '_ \ / _` / __| | | | |_| \__ \ |_| | | | | | (_| \__ \ |_| \__,_|___/\__|_|_|_| |_|\__, |___/ - |___/"; + |___/ -const WATCH_MODE_HELP_MESSAGE: &str = "Commands available to you in watch mode: - hint - prints the current exercise's hint - clear - clears the screen - quit - quits watch mode - ! - executes a command, like `!rustc --explain E0381` - help - displays this help message - -Watch mode automatically re-evaluates the current exercise -when you edit a file's contents."; +The `exercises` directory wasn't found in the current directory. +If you are just starting with Rustlings, run the command `rustlings init` to initialize it."; diff --git a/src/progress_bar.rs b/src/progress_bar.rs new file mode 100644 index 00000000..4a54170a --- /dev/null +++ b/src/progress_bar.rs @@ -0,0 +1,100 @@ +use anyhow::{bail, Result}; +use ratatui::text::{Line, Span}; +use std::fmt::Write; + +const PREFIX: &str = "Progress: ["; +const PREFIX_WIDTH: u16 = PREFIX.len() as u16; +// Leaving the last char empty (_) for `total` > 99. +const POSTFIX_WIDTH: u16 = "] xxx/xx exercises_".len() as u16; +const WRAPPER_WIDTH: u16 = PREFIX_WIDTH + POSTFIX_WIDTH; +const MIN_LINE_WIDTH: u16 = WRAPPER_WIDTH + 4; + +const PROGRESS_EXCEEDS_MAX_ERR: &str = + "The progress of the progress bar is higher than the maximum"; + +/// Terminal progress bar to be used when not using Ratataui. +pub fn progress_bar(progress: u16, total: u16, line_width: u16) -> Result { + use crossterm::style::Stylize; + + if progress > total { + bail!(PROGRESS_EXCEEDS_MAX_ERR); + } + + if line_width < MIN_LINE_WIDTH { + return Ok(format!("Progress: {progress}/{total} exercises")); + } + + let mut line = String::with_capacity(usize::from(line_width)); + line.push_str(PREFIX); + + let width = line_width - WRAPPER_WIDTH; + let filled = (width * progress) / total; + + let mut green_part = String::with_capacity(usize::from(filled + 1)); + for _ in 0..filled { + green_part.push('#'); + } + + if filled < width { + green_part.push('>'); + } + write!(line, "{}", green_part.green()).unwrap(); + + let width_minus_filled = width - filled; + if width_minus_filled > 1 { + let red_part_width = width_minus_filled - 1; + let mut red_part = String::with_capacity(usize::from(red_part_width)); + for _ in 0..red_part_width { + red_part.push('-'); + } + write!(line, "{}", red_part.red()).unwrap(); + } + + writeln!(line, "] {progress:>3}/{total} exercises").unwrap(); + + Ok(line) +} + +/// Progress bar to be used with Ratataui. +// Not using Ratatui's Gauge widget to keep the progress bar consistent. +pub fn progress_bar_ratatui(progress: u16, total: u16, line_width: u16) -> Result> { + use ratatui::style::Stylize; + + if progress > total { + bail!(PROGRESS_EXCEEDS_MAX_ERR); + } + + if line_width < MIN_LINE_WIDTH { + return Ok(Line::raw(format!("Progress: {progress}/{total} exercises"))); + } + + let mut spans = Vec::with_capacity(4); + spans.push(Span::raw(PREFIX)); + + let width = line_width - WRAPPER_WIDTH; + let filled = (width * progress) / total; + + let mut green_part = String::with_capacity(usize::from(filled + 1)); + for _ in 0..filled { + green_part.push('#'); + } + + if filled < width { + green_part.push('>'); + } + spans.push(green_part.green()); + + let width_minus_filled = width - filled; + if width_minus_filled > 1 { + let red_part_width = width_minus_filled - 1; + let mut red_part = String::with_capacity(usize::from(red_part_width)); + for _ in 0..red_part_width { + red_part.push('-'); + } + spans.push(red_part.red()); + } + + spans.push(Span::raw(format!("] {progress:>3}/{total} exercises"))); + + Ok(Line::from(spans)) +} diff --git a/src/project.rs b/src/project.rs deleted file mode 100644 index 0f56de96..00000000 --- a/src/project.rs +++ /dev/null @@ -1,83 +0,0 @@ -use anyhow::{Context, Result}; -use serde::Serialize; -use std::env; -use std::path::PathBuf; -use std::process::{Command, Stdio}; - -use crate::exercise::Exercise; - -/// Contains the structure of resulting rust-project.json file -/// and functions to build the data required to create the file -#[derive(Serialize)] -struct RustAnalyzerProject { - sysroot_src: PathBuf, - crates: Vec, -} - -#[derive(Serialize)] -struct Crate { - root_module: PathBuf, - edition: &'static str, - // Not used, but required in the JSON file. - deps: Vec<()>, - // Only `test` is used for all crates. - // Therefore, an array is used instead of a `Vec`. - cfg: [&'static str; 1], -} - -impl RustAnalyzerProject { - fn build(exercises: Vec) -> Result { - let crates = exercises - .into_iter() - .map(|exercise| Crate { - root_module: exercise.path, - edition: "2021", - deps: Vec::new(), - // This allows rust_analyzer to work inside `#[test]` blocks - cfg: ["test"], - }) - .collect(); - - if let Some(path) = env::var_os("RUST_SRC_PATH") { - return Ok(Self { - sysroot_src: PathBuf::from(path), - crates, - }); - } - - let toolchain = Command::new("rustc") - .arg("--print") - .arg("sysroot") - .stderr(Stdio::inherit()) - .output() - .context("Failed to get the sysroot from `rustc`. Do you have `rustc` installed?")? - .stdout; - - let toolchain = - String::from_utf8(toolchain).context("The toolchain path is invalid UTF8")?; - let toolchain = toolchain.trim_end(); - println!("Determined toolchain: {toolchain}\n"); - - let mut sysroot_src = PathBuf::with_capacity(256); - sysroot_src.extend([toolchain, "lib", "rustlib", "src", "rust", "library"]); - - Ok(Self { - sysroot_src, - crates, - }) - } -} - -/// Write `rust-project.json` to disk. -pub fn write_project_json(exercises: Vec) -> Result<()> { - let content = RustAnalyzerProject::build(exercises)?; - - // Using the capacity 2^14 since the file length in bytes is higher than 2^13. - // The final length is not known exactly because it depends on the user's sysroot path, - // the current number of exercises etc. - let mut buf = Vec::with_capacity(1 << 14); - serde_json::to_writer(&mut buf, &content)?; - std::fs::write("rust-project.json", buf)?; - - Ok(()) -} diff --git a/src/run.rs b/src/run.rs index 6dd0388f..899d0a94 100644 --- a/src/run.rs +++ b/src/run.rs @@ -1,75 +1,51 @@ -use std::process::Command; -use std::time::Duration; +use anyhow::{bail, Result}; +use crossterm::style::{style, Stylize}; +use std::io::{self, Write}; -use crate::exercise::{Exercise, Mode}; -use crate::verify::test; -use indicatif::ProgressBar; +use crate::{ + app_state::{AppState, ExercisesProgress}, + exercise::{RunnableExercise, OUTPUT_CAPACITY}, + terminal_link::TerminalFileLink, +}; -// Invoke the rust compiler on the path of the given exercise, -// and run the ensuing binary. -// The verbose argument helps determine whether or not to show -// the output from the test harnesses (if the mode of the exercise is test) -pub fn run(exercise: &Exercise, verbose: bool) -> Result<(), ()> { - match exercise.mode { - Mode::Test => test(exercise, verbose)?, - Mode::Compile => compile_and_run(exercise)?, - Mode::Clippy => compile_and_run(exercise)?, +pub fn run(app_state: &mut AppState) -> Result<()> { + let exercise = app_state.current_exercise(); + let mut output = Vec::with_capacity(OUTPUT_CAPACITY); + let success = exercise.run_exercise(&mut output, app_state.target_dir())?; + + let mut stdout = io::stdout().lock(); + stdout.write_all(&output)?; + + if !success { + app_state.set_pending(app_state.current_exercise_ind())?; + + bail!( + "Ran {} with errors", + app_state.current_exercise().terminal_link(), + ); } + + writeln!( + stdout, + "{}{}", + "✓ Successfully ran ".green(), + exercise.path.green(), + )?; + + if let Some(solution_path) = app_state.current_solution_path()? { + println!( + "\nA solution file can be found at {}\n", + style(TerminalFileLink(&solution_path)).underlined().green(), + ); + } + + match app_state.done_current_exercise(&mut stdout)? { + ExercisesProgress::AllDone => (), + ExercisesProgress::CurrentPending | ExercisesProgress::NewPending => println!( + "Next exercise: {}", + app_state.current_exercise().terminal_link(), + ), + } + Ok(()) } - -// Resets the exercise by stashing the changes. -pub fn reset(exercise: &Exercise) -> Result<(), ()> { - let command = Command::new("git") - .arg("stash") - .arg("--") - .arg(&exercise.path) - .spawn(); - - match command { - Ok(_) => Ok(()), - Err(_) => Err(()), - } -} - -// Invoke the rust compiler on the path of the given exercise -// and run the ensuing binary. -// This is strictly for non-test binaries, so output is displayed -fn compile_and_run(exercise: &Exercise) -> Result<(), ()> { - let progress_bar = ProgressBar::new_spinner(); - progress_bar.set_message(format!("Compiling {exercise}...")); - progress_bar.enable_steady_tick(Duration::from_millis(100)); - - let compilation_result = exercise.compile(); - let compilation = match compilation_result { - Ok(compilation) => compilation, - Err(output) => { - progress_bar.finish_and_clear(); - warn!( - "Compilation of {} failed!, Compiler error message:\n", - exercise - ); - println!("{}", output.stderr); - return Err(()); - } - }; - - progress_bar.set_message(format!("Running {exercise}...")); - let result = compilation.run(); - progress_bar.finish_and_clear(); - - match result { - Ok(output) => { - println!("{}", output.stdout); - success!("Successfully ran {}", exercise); - Ok(()) - } - Err(output) => { - println!("{}", output.stdout); - println!("{}", output.stderr); - - warn!("Ran {} with errors", exercise); - Err(()) - } - } -} diff --git a/src/terminal_link.rs b/src/terminal_link.rs new file mode 100644 index 00000000..9bea07d9 --- /dev/null +++ b/src/terminal_link.rs @@ -0,0 +1,26 @@ +use std::{ + fmt::{self, Display, Formatter}, + fs, +}; + +pub struct TerminalFileLink<'a>(pub &'a str); + +impl<'a> Display for TerminalFileLink<'a> { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + let path = fs::canonicalize(self.0); + + if let Some(path) = path.as_deref().ok().and_then(|path| path.to_str()) { + // Windows itself can't handle its verbatim paths. + #[cfg(windows)] + let path = if path.len() > 5 && &path[0..4] == r"\\?\" { + &path[4..] + } else { + path + }; + + write!(f, "\x1b]8;;file://{path}\x1b\\{}\x1b]8;;\x1b\\", self.0) + } else { + write!(f, "{}", self.0) + } + } +} diff --git a/src/ui.rs b/src/ui.rs deleted file mode 100644 index d8177b9f..00000000 --- a/src/ui.rs +++ /dev/null @@ -1,28 +0,0 @@ -macro_rules! print_emoji { - ($emoji:expr, $sign:expr, $color: ident, $fmt:literal, $ex:expr) => {{ - use console::{style, Emoji}; - use std::env; - let formatstr = format!($fmt, $ex); - if env::var("NO_EMOJI").is_ok() { - println!("{} {}", style($sign).$color(), style(formatstr).$color()); - } else { - println!( - "{} {}", - style(Emoji($emoji, $sign)).$color(), - style(formatstr).$color() - ); - } - }}; -} - -macro_rules! warn { - ($fmt:literal, $ex:expr) => {{ - print_emoji!("⚠️ ", "!", red, $fmt, $ex); - }}; -} - -macro_rules! success { - ($fmt:literal, $ex:expr) => {{ - print_emoji!("✅ ", "✓", green, $fmt, $ex); - }}; -} diff --git a/src/verify.rs b/src/verify.rs deleted file mode 100644 index dac25626..00000000 --- a/src/verify.rs +++ /dev/null @@ -1,242 +0,0 @@ -use crate::exercise::{CompiledExercise, Exercise, Mode, State}; -use console::style; -use indicatif::{ProgressBar, ProgressStyle}; -use std::{env, time::Duration}; - -// Verify that the provided container of Exercise objects -// can be compiled and run without any failures. -// Any such failures will be reported to the end user. -// If the Exercise being verified is a test, the verbose boolean -// determines whether or not the test harness outputs are displayed. -pub fn verify<'a>( - exercises: impl IntoIterator, - progress: (usize, usize), - verbose: bool, - success_hints: bool, -) -> Result<(), &'a Exercise> { - let (num_done, total) = progress; - let bar = ProgressBar::new(total as u64); - let mut percentage = num_done as f32 / total as f32 * 100.0; - bar.set_style( - ProgressStyle::default_bar() - .template("Progress: [{bar:60.green/red}] {pos}/{len} {msg}") - .expect("Progressbar template should be valid!") - .progress_chars("#>-"), - ); - bar.set_position(num_done as u64); - bar.set_message(format!("({percentage:.1} %)")); - - for exercise in exercises { - let compile_result = match exercise.mode { - Mode::Test => compile_and_test(exercise, RunMode::Interactive, verbose, success_hints), - Mode::Compile => compile_and_run_interactively(exercise, success_hints), - Mode::Clippy => compile_only(exercise, success_hints), - }; - if !compile_result.unwrap_or(false) { - return Err(exercise); - } - percentage += 100.0 / total as f32; - bar.inc(1); - bar.set_message(format!("({percentage:.1} %)")); - if bar.position() == total as u64 { - println!( - "Progress: You completed {} / {} exercises ({:.1} %).", - bar.position(), - total, - percentage - ); - bar.finish(); - } - } - Ok(()) -} - -#[derive(PartialEq, Eq)] -enum RunMode { - Interactive, - NonInteractive, -} - -// Compile and run the resulting test harness of the given Exercise -pub fn test(exercise: &Exercise, verbose: bool) -> Result<(), ()> { - compile_and_test(exercise, RunMode::NonInteractive, verbose, false)?; - Ok(()) -} - -// Invoke the rust compiler without running the resulting binary -fn compile_only(exercise: &Exercise, success_hints: bool) -> Result { - let progress_bar = ProgressBar::new_spinner(); - progress_bar.set_message(format!("Compiling {exercise}...")); - progress_bar.enable_steady_tick(Duration::from_millis(100)); - - let _ = compile(exercise, &progress_bar)?; - progress_bar.finish_and_clear(); - - Ok(prompt_for_completion(exercise, None, success_hints)) -} - -// Compile the given Exercise and run the resulting binary in an interactive mode -fn compile_and_run_interactively(exercise: &Exercise, success_hints: bool) -> Result { - let progress_bar = ProgressBar::new_spinner(); - progress_bar.set_message(format!("Compiling {exercise}...")); - progress_bar.enable_steady_tick(Duration::from_millis(100)); - - let compilation = compile(exercise, &progress_bar)?; - - progress_bar.set_message(format!("Running {exercise}...")); - let result = compilation.run(); - progress_bar.finish_and_clear(); - - let output = match result { - Ok(output) => output, - Err(output) => { - warn!("Ran {} with errors", exercise); - println!("{}", output.stdout); - println!("{}", output.stderr); - return Err(()); - } - }; - - Ok(prompt_for_completion( - exercise, - Some(output.stdout), - success_hints, - )) -} - -// Compile the given Exercise as a test harness and display -// the output if verbose is set to true -fn compile_and_test( - exercise: &Exercise, - run_mode: RunMode, - verbose: bool, - success_hints: bool, -) -> Result { - let progress_bar = ProgressBar::new_spinner(); - progress_bar.set_message(format!("Testing {exercise}...")); - progress_bar.enable_steady_tick(Duration::from_millis(100)); - - let compilation = compile(exercise, &progress_bar)?; - let result = compilation.run(); - progress_bar.finish_and_clear(); - - match result { - Ok(output) => { - if verbose { - println!("{}", output.stdout); - } - if run_mode == RunMode::Interactive { - Ok(prompt_for_completion(exercise, None, success_hints)) - } else { - Ok(true) - } - } - Err(output) => { - warn!( - "Testing of {} failed! Please try again. Here's the output:", - exercise - ); - println!("{}", output.stdout); - Err(()) - } - } -} - -// Compile the given Exercise and return an object with information -// about the state of the compilation -fn compile<'a>( - exercise: &'a Exercise, - progress_bar: &ProgressBar, -) -> Result, ()> { - let compilation_result = exercise.compile(); - - match compilation_result { - Ok(compilation) => Ok(compilation), - Err(output) => { - progress_bar.finish_and_clear(); - warn!( - "Compiling of {} failed! Please try again. Here's the output:", - exercise - ); - println!("{}", output.stderr); - Err(()) - } - } -} - -fn prompt_for_completion( - exercise: &Exercise, - prompt_output: Option, - success_hints: bool, -) -> bool { - let context = match exercise.state() { - State::Done => return true, - State::Pending(context) => context, - }; - match exercise.mode { - Mode::Compile => success!("Successfully ran {}!", exercise), - Mode::Test => success!("Successfully tested {}!", exercise), - Mode::Clippy => success!("Successfully compiled {}!", exercise), - } - - let no_emoji = env::var("NO_EMOJI").is_ok(); - - let clippy_success_msg = if no_emoji { - "The code is compiling, and Clippy is happy!" - } else { - "The code is compiling, and 📎 Clippy 📎 is happy!" - }; - - let success_msg = match exercise.mode { - Mode::Compile => "The code is compiling!", - Mode::Test => "The code is compiling, and the tests pass!", - Mode::Clippy => clippy_success_msg, - }; - - if no_emoji { - println!("\n~*~ {success_msg} ~*~\n"); - } else { - println!("\n🎉 🎉 {success_msg} 🎉 🎉\n"); - } - - if let Some(output) = prompt_output { - println!( - "Output:\n{separator}\n{output}\n{separator}\n", - separator = separator(), - ); - } - if success_hints { - println!( - "Hints:\n{separator}\n{}\n{separator}\n", - exercise.hint, - separator = separator(), - ); - } - - println!("You can keep working on this exercise,"); - println!( - "or jump into the next one by removing the {} comment:", - style("`I AM NOT DONE`").bold() - ); - println!(); - for context_line in context { - let formatted_line = if context_line.important { - format!("{}", style(context_line.line).bold()) - } else { - context_line.line - }; - - println!( - "{:>2} {} {}", - style(context_line.number).blue().bold(), - style("|").blue(), - formatted_line, - ); - } - - false -} - -fn separator() -> console::StyledObject<&'static str> { - style("====================").bold() -} diff --git a/src/watch.rs b/src/watch.rs new file mode 100644 index 00000000..88a12301 --- /dev/null +++ b/src/watch.rs @@ -0,0 +1,128 @@ +use anyhow::{Error, Result}; +use notify_debouncer_mini::{ + new_debouncer, + notify::{self, RecursiveMode}, +}; +use std::{ + io::{self, Write}, + path::Path, + sync::mpsc::channel, + thread, + time::Duration, +}; + +use crate::app_state::{AppState, ExercisesProgress}; + +use self::{ + notify_event::NotifyEventHandler, + state::WatchState, + terminal_event::{terminal_event_handler, InputEvent}, +}; + +mod notify_event; +mod state; +mod terminal_event; + +enum WatchEvent { + Input(InputEvent), + FileChange { exercise_ind: usize }, + TerminalResize, + NotifyErr(notify::Error), + TerminalEventErr(io::Error), +} + +/// Returned by the watch mode to indicate what to do afterwards. +#[must_use] +pub enum WatchExit { + /// Exit the program. + Shutdown, + /// Enter the list mode and restart the watch mode afterwards. + List, +} + +/// `notify_exercise_names` as None activates the manual run mode. +pub fn watch( + app_state: &mut AppState, + notify_exercise_names: Option<&'static [&'static [u8]]>, +) -> Result { + let (tx, rx) = channel(); + + let mut manual_run = false; + // Prevent dropping the guard until the end of the function. + // Otherwise, the file watcher exits. + let _debouncer_guard = if let Some(exercise_names) = notify_exercise_names { + let mut debouncer = new_debouncer( + Duration::from_millis(200), + NotifyEventHandler { + tx: tx.clone(), + exercise_names, + }, + ) + .inspect_err(|_| eprintln!("{NOTIFY_ERR}"))?; + debouncer + .watcher() + .watch(Path::new("exercises"), RecursiveMode::Recursive) + .inspect_err(|_| eprintln!("{NOTIFY_ERR}"))?; + + Some(debouncer) + } else { + manual_run = true; + None + }; + + let mut watch_state = WatchState::new(app_state, manual_run); + + watch_state.run_current_exercise()?; + + thread::spawn(move || terminal_event_handler(tx, manual_run)); + + while let Ok(event) = rx.recv() { + match event { + WatchEvent::Input(InputEvent::Next) => match watch_state.next_exercise()? { + ExercisesProgress::AllDone => break, + ExercisesProgress::CurrentPending => watch_state.render()?, + ExercisesProgress::NewPending => watch_state.run_current_exercise()?, + }, + WatchEvent::Input(InputEvent::Hint) => { + watch_state.show_hint()?; + } + WatchEvent::Input(InputEvent::List) => { + return Ok(WatchExit::List); + } + WatchEvent::Input(InputEvent::Quit) => { + watch_state.into_writer().write_all(QUIT_MSG)?; + break; + } + WatchEvent::Input(InputEvent::Run) => watch_state.run_current_exercise()?, + WatchEvent::Input(InputEvent::Unrecognized) => watch_state.render()?, + WatchEvent::FileChange { exercise_ind } => { + watch_state.handle_file_change(exercise_ind)?; + } + WatchEvent::TerminalResize => { + watch_state.render()?; + } + WatchEvent::NotifyErr(e) => { + watch_state.into_writer().write_all(NOTIFY_ERR.as_bytes())?; + return Err(Error::from(e)); + } + WatchEvent::TerminalEventErr(e) => { + return Err(Error::from(e).context("Terminal event listener failed")); + } + } + } + + Ok(WatchExit::Shutdown) +} + +const QUIT_MSG: &[u8] = b" +We hope you're enjoying learning Rust! +If you want to continue working on the exercises at a later point, you can simply run `rustlings` again. +"; + +const NOTIFY_ERR: &str = " +The automatic detection of exercise file changes failed :( +Please try running `rustlings` again. + +If you keep getting this error, run `rustlings --manual-run` to deactivate the file watcher. +You need to manually trigger running the current exercise using `r` then. +"; diff --git a/src/watch/notify_event.rs b/src/watch/notify_event.rs new file mode 100644 index 00000000..74716409 --- /dev/null +++ b/src/watch/notify_event.rs @@ -0,0 +1,52 @@ +use notify_debouncer_mini::{DebounceEventResult, DebouncedEventKind}; +use std::sync::mpsc::Sender; + +use super::WatchEvent; + +pub struct NotifyEventHandler { + pub tx: Sender, + /// Used to report which exercise was modified. + pub exercise_names: &'static [&'static [u8]], +} + +impl notify_debouncer_mini::DebounceEventHandler for NotifyEventHandler { + fn handle_event(&mut self, input_event: DebounceEventResult) { + let output_event = match input_event { + Ok(input_event) => { + let Some(exercise_ind) = input_event + .iter() + .filter_map(|input_event| { + if input_event.kind != DebouncedEventKind::Any { + return None; + } + + let file_name = input_event.path.file_name()?.to_str()?.as_bytes(); + + if file_name.len() < 4 { + return None; + } + let (file_name_without_ext, ext) = file_name.split_at(file_name.len() - 3); + + if ext != b".rs" { + return None; + } + + self.exercise_names + .iter() + .position(|exercise_name| *exercise_name == file_name_without_ext) + }) + .min() + else { + return; + }; + + WatchEvent::FileChange { exercise_ind } + } + Err(e) => WatchEvent::NotifyErr(e), + }; + + // An error occurs when the receiver is dropped. + // After dropping the receiver, the debouncer guard should also be dropped. + let _ = self.tx.send(output_event); + } +} diff --git a/src/watch/state.rs b/src/watch/state.rs new file mode 100644 index 00000000..78af30a4 --- /dev/null +++ b/src/watch/state.rs @@ -0,0 +1,174 @@ +use anyhow::Result; +use crossterm::{ + style::{style, Stylize}, + terminal, +}; +use std::io::{self, StdoutLock, Write}; + +use crate::{ + app_state::{AppState, ExercisesProgress}, + clear_terminal, + exercise::{RunnableExercise, OUTPUT_CAPACITY}, + progress_bar::progress_bar, + terminal_link::TerminalFileLink, +}; + +#[derive(PartialEq, Eq)] +enum DoneStatus { + DoneWithSolution(String), + DoneWithoutSolution, + Pending, +} + +pub struct WatchState<'a> { + writer: StdoutLock<'a>, + app_state: &'a mut AppState, + output: Vec, + show_hint: bool, + done_status: DoneStatus, + manual_run: bool, +} + +impl<'a> WatchState<'a> { + pub fn new(app_state: &'a mut AppState, manual_run: bool) -> Self { + let writer = io::stdout().lock(); + + Self { + writer, + app_state, + output: Vec::with_capacity(OUTPUT_CAPACITY), + show_hint: false, + done_status: DoneStatus::Pending, + manual_run, + } + } + + #[inline] + pub fn into_writer(self) -> StdoutLock<'a> { + self.writer + } + + pub fn run_current_exercise(&mut self) -> Result<()> { + self.show_hint = false; + + let success = self + .app_state + .current_exercise() + .run_exercise(&mut self.output, self.app_state.target_dir())?; + if success { + self.done_status = + if let Some(solution_path) = self.app_state.current_solution_path()? { + DoneStatus::DoneWithSolution(solution_path) + } else { + DoneStatus::DoneWithoutSolution + }; + } else { + self.app_state + .set_pending(self.app_state.current_exercise_ind())?; + + self.done_status = DoneStatus::Pending; + } + + self.render() + } + + pub fn handle_file_change(&mut self, exercise_ind: usize) -> Result<()> { + // Don't skip exercises on file changes to avoid confusion from missing exercises. + // Skipping exercises must be explicit in the interactive list. + // But going back to an earlier exercise on file change is fine. + if self.app_state.current_exercise_ind() < exercise_ind { + return Ok(()); + } + + self.app_state.set_current_exercise_ind(exercise_ind)?; + self.run_current_exercise() + } + + /// Move on to the next exercise if the current one is done. + pub fn next_exercise(&mut self) -> Result { + if self.done_status == DoneStatus::Pending { + return Ok(ExercisesProgress::CurrentPending); + } + + self.app_state.done_current_exercise(&mut self.writer) + } + + fn show_prompt(&mut self) -> io::Result<()> { + self.writer.write_all(b"\n")?; + + if self.manual_run { + write!(self.writer, "{}:run / ", 'r'.bold())?; + } + + if self.done_status != DoneStatus::Pending { + write!(self.writer, "{}:{} / ", 'n'.bold(), "next".underlined())?; + } + + if !self.show_hint { + write!(self.writer, "{}:hint / ", 'h'.bold())?; + } + + write!(self.writer, "{}:list / {}:quit ? ", 'l'.bold(), 'q'.bold())?; + + self.writer.flush() + } + + pub fn render(&mut self) -> Result<()> { + // Prevent having the first line shifted if clearing wasn't successful. + self.writer.write_all(b"\n")?; + + clear_terminal(&mut self.writer)?; + + self.writer.write_all(&self.output)?; + self.writer.write_all(b"\n")?; + + if self.show_hint { + writeln!( + self.writer, + "{}\n{}\n", + "Hint".bold().cyan().underlined(), + self.app_state.current_exercise().hint, + )?; + } + + if self.done_status != DoneStatus::Pending { + writeln!( + self.writer, + "{}\n", + "Exercise done ✓ +When you are done experimenting, enter `n` to move on to the next exercise 🦀" + .bold() + .green(), + )?; + } + + if let DoneStatus::DoneWithSolution(solution_path) = &self.done_status { + writeln!( + self.writer, + "A solution file can be found at {}\n", + style(TerminalFileLink(solution_path)).underlined().green(), + )?; + } + + let line_width = terminal::size()?.0; + let progress_bar = progress_bar( + self.app_state.n_done(), + self.app_state.exercises().len() as u16, + line_width, + )?; + writeln!( + self.writer, + "{progress_bar}Current exercise: {}", + self.app_state.current_exercise().terminal_link(), + )?; + + self.show_prompt()?; + + Ok(()) + } + + pub fn show_hint(&mut self) -> Result<()> { + self.show_hint = true; + self.render() + } +} diff --git a/src/watch/terminal_event.rs b/src/watch/terminal_event.rs new file mode 100644 index 00000000..f54af17a --- /dev/null +++ b/src/watch/terminal_event.rs @@ -0,0 +1,86 @@ +use crossterm::event::{self, Event, KeyCode, KeyEventKind, KeyModifiers}; +use std::sync::mpsc::Sender; + +use super::WatchEvent; + +pub enum InputEvent { + Run, + Next, + Hint, + List, + Quit, + Unrecognized, +} + +pub fn terminal_event_handler(tx: Sender, manual_run: bool) { + // Only send `Unrecognized` on ENTER if the last input wasn't valid. + let mut last_input_valid = false; + + let last_input_event = loop { + let terminal_event = match event::read() { + Ok(v) => v, + Err(e) => { + // If `send` returns an error, then the receiver is dropped and + // a shutdown has been already initialized. + let _ = tx.send(WatchEvent::TerminalEventErr(e)); + return; + } + }; + + match terminal_event { + Event::Key(key) => { + match key.kind { + KeyEventKind::Release | KeyEventKind::Repeat => continue, + KeyEventKind::Press => (), + } + + if key.modifiers != KeyModifiers::NONE { + last_input_valid = false; + continue; + } + + let input_event = match key.code { + KeyCode::Enter => { + if last_input_valid { + continue; + } + + InputEvent::Unrecognized + } + KeyCode::Char(c) => { + let input_event = match c { + 'n' => InputEvent::Next, + 'h' => InputEvent::Hint, + 'l' => break InputEvent::List, + 'q' => break InputEvent::Quit, + 'r' if manual_run => InputEvent::Run, + _ => { + last_input_valid = false; + continue; + } + }; + + last_input_valid = true; + input_event + } + _ => { + last_input_valid = false; + continue; + } + }; + + if tx.send(WatchEvent::Input(input_event)).is_err() { + return; + } + } + Event::Resize(_, _) => { + if tx.send(WatchEvent::TerminalResize).is_err() { + return; + } + } + Event::FocusGained | Event::FocusLost | Event::Mouse(_) | Event::Paste(_) => continue, + } + }; + + let _ = tx.send(WatchEvent::Input(last_input_event)); +} diff --git a/tests/fixture/failure/Cargo.toml b/tests/fixture/failure/Cargo.toml new file mode 100644 index 00000000..7ee2f068 --- /dev/null +++ b/tests/fixture/failure/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "failure" +edition = "2021" +publish = false + +[[bin]] +name = "compFailure" +path = "exercises/compFailure.rs" + +[[bin]] +name = "compNoExercise" +path = "exercises/compNoExercise.rs" + +[[bin]] +name = "testFailure" +path = "exercises/testFailure.rs" + +[[bin]] +name = "testNotPassed" +path = "exercises/testNotPassed.rs" diff --git a/tests/fixture/failure/compNoExercise.rs b/tests/fixture/failure/compNoExercise.rs deleted file mode 100644 index f79c691f..00000000 --- a/tests/fixture/failure/compNoExercise.rs +++ /dev/null @@ -1,2 +0,0 @@ -fn main() { -} diff --git a/tests/fixture/failure/compFailure.rs b/tests/fixture/failure/exercises/compFailure.rs similarity index 100% rename from tests/fixture/failure/compFailure.rs rename to tests/fixture/failure/exercises/compFailure.rs diff --git a/tests/fixture/failure/exercises/compNoExercise.rs b/tests/fixture/failure/exercises/compNoExercise.rs new file mode 100644 index 00000000..f328e4d9 --- /dev/null +++ b/tests/fixture/failure/exercises/compNoExercise.rs @@ -0,0 +1 @@ +fn main() {} diff --git a/tests/fixture/failure/testFailure.rs b/tests/fixture/failure/exercises/testFailure.rs similarity index 75% rename from tests/fixture/failure/testFailure.rs rename to tests/fixture/failure/exercises/testFailure.rs index b33a5d28..fcbcf90a 100644 --- a/tests/fixture/failure/testFailure.rs +++ b/tests/fixture/failure/exercises/testFailure.rs @@ -1,3 +1,5 @@ +fn main() {} + #[test] fn passing() { asset!(true); diff --git a/tests/fixture/failure/testNotPassed.rs b/tests/fixture/failure/exercises/testNotPassed.rs similarity index 77% rename from tests/fixture/failure/testNotPassed.rs rename to tests/fixture/failure/exercises/testNotPassed.rs index a9fe88dd..de0d61c0 100644 --- a/tests/fixture/failure/testNotPassed.rs +++ b/tests/fixture/failure/exercises/testNotPassed.rs @@ -1,3 +1,5 @@ +fn main() {} + #[test] fn not_passing() { assert!(false); diff --git a/tests/fixture/failure/info.toml b/tests/fixture/failure/info.toml index e5949f9b..554607a8 100644 --- a/tests/fixture/failure/info.toml +++ b/tests/fixture/failure/info.toml @@ -1,11 +1,10 @@ +format_version = 1 + [[exercises]] name = "compFailure" -path = "compFailure.rs" -mode = "compile" +test = false hint = "" [[exercises]] name = "testFailure" -path = "testFailure.rs" -mode = "test" hint = "Hello!" diff --git a/tests/fixture/state/Cargo.toml b/tests/fixture/state/Cargo.toml new file mode 100644 index 00000000..adbd8ab1 --- /dev/null +++ b/tests/fixture/state/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "state" +edition = "2021" +publish = false + +[[bin]] +name = "pending_exercise" +path = "exercises/pending_exercise.rs" + +[[bin]] +name = "pending_test_exercise" +path = "exercises/pending_test_exercise.rs" + +[[bin]] +name = "finished_exercise" +path = "exercises/finished_exercise.rs" diff --git a/tests/fixture/state/exercises/finished_exercise.rs b/tests/fixture/state/exercises/finished_exercise.rs new file mode 100644 index 00000000..f328e4d9 --- /dev/null +++ b/tests/fixture/state/exercises/finished_exercise.rs @@ -0,0 +1 @@ +fn main() {} diff --git a/tests/fixture/state/exercises/pending_exercise.rs b/tests/fixture/state/exercises/pending_exercise.rs new file mode 100644 index 00000000..f328e4d9 --- /dev/null +++ b/tests/fixture/state/exercises/pending_exercise.rs @@ -0,0 +1 @@ +fn main() {} diff --git a/tests/fixture/state/pending_test_exercise.rs b/tests/fixture/state/exercises/pending_test_exercise.rs similarity index 60% rename from tests/fixture/state/pending_test_exercise.rs rename to tests/fixture/state/exercises/pending_test_exercise.rs index 8756f02d..718e1dbb 100644 --- a/tests/fixture/state/pending_test_exercise.rs +++ b/tests/fixture/state/exercises/pending_test_exercise.rs @@ -1,4 +1,4 @@ -// I AM NOT DONE +fn main() {} #[test] fn it_works() {} diff --git a/tests/fixture/state/finished_exercise.rs b/tests/fixture/state/finished_exercise.rs deleted file mode 100644 index 016b827c..00000000 --- a/tests/fixture/state/finished_exercise.rs +++ /dev/null @@ -1,5 +0,0 @@ -// fake_exercise - -fn main() { - -} diff --git a/tests/fixture/state/info.toml b/tests/fixture/state/info.toml index 547b3a48..ff0b932e 100644 --- a/tests/fixture/state/info.toml +++ b/tests/fixture/state/info.toml @@ -1,18 +1,15 @@ +format_version = 1 + [[exercises]] name = "pending_exercise" -path = "pending_exercise.rs" -mode = "compile" +test = false hint = """""" [[exercises]] name = "pending_test_exercise" -path = "pending_test_exercise.rs" -mode = "test" hint = """""" [[exercises]] name = "finished_exercise" -path = "finished_exercise.rs" -mode = "compile" +test = false hint = """""" - diff --git a/tests/fixture/state/pending_exercise.rs b/tests/fixture/state/pending_exercise.rs deleted file mode 100644 index f579d0b4..00000000 --- a/tests/fixture/state/pending_exercise.rs +++ /dev/null @@ -1,7 +0,0 @@ -// fake_exercise - -// I AM NOT DONE - -fn main() { - -} diff --git a/tests/fixture/success/Cargo.toml b/tests/fixture/success/Cargo.toml new file mode 100644 index 00000000..028cf35a --- /dev/null +++ b/tests/fixture/success/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "success" +edition = "2021" +publish = false + +[[bin]] +name = "compSuccess" +path = "exercises/compSuccess.rs" + +[[bin]] +name = "testSuccess" +path = "exercises/testSuccess.rs" diff --git a/tests/fixture/success/compSuccess.rs b/tests/fixture/success/compSuccess.rs deleted file mode 100644 index f79c691f..00000000 --- a/tests/fixture/success/compSuccess.rs +++ /dev/null @@ -1,2 +0,0 @@ -fn main() { -} diff --git a/tests/fixture/success/exercises/compSuccess.rs b/tests/fixture/success/exercises/compSuccess.rs new file mode 100644 index 00000000..f328e4d9 --- /dev/null +++ b/tests/fixture/success/exercises/compSuccess.rs @@ -0,0 +1 @@ +fn main() {} diff --git a/tests/fixture/success/testSuccess.rs b/tests/fixture/success/exercises/testSuccess.rs similarity index 86% rename from tests/fixture/success/testSuccess.rs rename to tests/fixture/success/exercises/testSuccess.rs index 7139b50b..4296cf61 100644 --- a/tests/fixture/success/testSuccess.rs +++ b/tests/fixture/success/exercises/testSuccess.rs @@ -1,3 +1,5 @@ +fn main() {} + #[test] fn passing() { println!("THIS TEST TOO SHALL PASS"); diff --git a/tests/fixture/success/info.toml b/tests/fixture/success/info.toml index 68d35388..d66d7d47 100644 --- a/tests/fixture/success/info.toml +++ b/tests/fixture/success/info.toml @@ -1,11 +1,10 @@ +format_version = 1 + [[exercises]] name = "compSuccess" -path = "compSuccess.rs" -mode = "compile" +test = false hint = """""" [[exercises]] name = "testSuccess" -path = "testSuccess.rs" -mode = "test" hint = """""" diff --git a/tests/integration_tests.rs b/tests/integration_tests.rs index d1694a39..7d30467b 100644 --- a/tests/integration_tests.rs +++ b/tests/integration_tests.rs @@ -1,16 +1,6 @@ use assert_cmd::prelude::*; -use glob::glob; -use predicates::boolean::PredicateBooleanExt; -use std::fs::File; -use std::io::Read; use std::process::Command; -#[test] -fn runs_without_arguments() { - let mut cmd = Command::cargo_bin("rustlings").unwrap(); - cmd.assert().success(); -} - #[test] fn fails_when_in_wrong_dir() { Command::cargo_bin("rustlings") @@ -20,26 +10,6 @@ fn fails_when_in_wrong_dir() { .code(1); } -#[test] -fn verify_all_success() { - Command::cargo_bin("rustlings") - .unwrap() - .arg("verify") - .current_dir("tests/fixture/success") - .assert() - .success(); -} - -#[test] -fn verify_fails_if_some_fails() { - Command::cargo_bin("rustlings") - .unwrap() - .arg("verify") - .current_dir("tests/fixture/failure") - .assert() - .code(1); -} - #[test] fn run_single_compile_success() { Command::cargo_bin("rustlings") @@ -90,19 +60,6 @@ fn run_single_test_not_passed() { .code(1); } -#[test] -fn run_single_test_no_filename() { - Command::cargo_bin("rustlings") - .unwrap() - .arg("run") - .current_dir("tests/fixture/") - .assert() - .code(2) - .stderr(predicates::str::contains( - "required arguments were not provided", - )); -} - #[test] fn run_single_test_no_exercise() { Command::cargo_bin("rustlings") @@ -145,31 +102,6 @@ fn get_hint_for_single_test() { .stdout("Hello!\n"); } -#[test] -fn all_exercises_require_confirmation() { - for exercise in glob("exercises/**/*.rs").unwrap() { - let path = exercise.unwrap(); - if path.file_name().unwrap() == "mod.rs" { - continue; - } - let source = { - let mut file = File::open(&path).unwrap(); - let mut s = String::new(); - file.read_to_string(&mut s).unwrap(); - s - }; - source - .matches("// I AM NOT DONE") - .next() - .unwrap_or_else(|| { - panic!( - "There should be an `I AM NOT DONE` annotation in {:?}", - path - ) - }); - } -} - #[test] fn run_compile_exercise_does_not_prompt() { Command::cargo_bin("rustlings") @@ -177,8 +109,7 @@ fn run_compile_exercise_does_not_prompt() { .args(["run", "pending_exercise"]) .current_dir("tests/fixture/state") .assert() - .code(0) - .stdout(predicates::str::contains("I AM NOT DONE").not()); + .code(0); } #[test] @@ -188,82 +119,16 @@ fn run_test_exercise_does_not_prompt() { .args(["run", "pending_test_exercise"]) .current_dir("tests/fixture/state") .assert() - .code(0) - .stdout(predicates::str::contains("I AM NOT DONE").not()); + .code(0); } #[test] fn run_single_test_success_with_output() { Command::cargo_bin("rustlings") .unwrap() - .args(["--nocapture", "run", "testSuccess"]) + .args(["run", "testSuccess"]) .current_dir("tests/fixture/success/") .assert() .code(0) .stdout(predicates::str::contains("THIS TEST TOO SHALL PASS")); } - -#[test] -fn run_single_test_success_without_output() { - Command::cargo_bin("rustlings") - .unwrap() - .args(["run", "testSuccess"]) - .current_dir("tests/fixture/success/") - .assert() - .code(0) - .stdout(predicates::str::contains("THIS TEST TOO SHALL PASS").not()); -} - -#[test] -fn run_rustlings_list() { - Command::cargo_bin("rustlings") - .unwrap() - .args(["list"]) - .current_dir("tests/fixture/success") - .assert() - .success(); -} - -#[test] -fn run_rustlings_list_no_pending() { - Command::cargo_bin("rustlings") - .unwrap() - .args(["list"]) - .current_dir("tests/fixture/success") - .assert() - .success() - .stdout(predicates::str::contains("Pending").not()); -} - -#[test] -fn run_rustlings_list_both_done_and_pending() { - Command::cargo_bin("rustlings") - .unwrap() - .args(["list"]) - .current_dir("tests/fixture/state") - .assert() - .success() - .stdout(predicates::str::contains("Done").and(predicates::str::contains("Pending"))); -} - -#[test] -fn run_rustlings_list_without_pending() { - Command::cargo_bin("rustlings") - .unwrap() - .args(["list", "--solved"]) - .current_dir("tests/fixture/state") - .assert() - .success() - .stdout(predicates::str::contains("Pending").not()); -} - -#[test] -fn run_rustlings_list_without_done() { - Command::cargo_bin("rustlings") - .unwrap() - .args(["list", "--unsolved"]) - .current_dir("tests/fixture/state") - .assert() - .success() - .stdout(predicates::str::contains("Done").not()); -}